The Higher Education and Research forge

Home My Page Projects Code Snippets Project Openings EMULSION public releases
Summary Activity Surveys SCM Listes Sympa

SCM Repository

2fc7d5977eee09f56670f6faa4a7f8d01d3946d8
1 """A Python implementation of the EMuLSion framework (Epidemiologic
2 MUlti-Level SImulatiONs).
4 Classes and functions for actions.
5 """
8 # EMULSION (Epidemiological Multi-Level Simulation framework)
9 # ===========================================================
10
11 # Contributors and contact:
12 # -------------------------
13
14 #     - Sébastien Picault (sebastien.picault@inra.fr)
15 #     - Yu-Lin Huang
16 #     - Vianney Sicard
17 #     - Sandie Arnoux
18 #     - Gaël Beaunée
19 #     - Pauline Ezanno (pauline.ezanno@inra.fr)
20
21 #     BIOEPAR, INRA, Oniris, Atlanpole La Chantrerie,
22 #     Nantes CS 44307 CEDEX, France
23
24
25 # How to cite:
26 # ------------
27
28 #     S. Picault, Y.-L. Huang, V. Sicard, P. Ezanno (2017). "Enhancing
29 #     Sustainability of Complex Epidemiological Models through a Generic
30 #     Multilevel Agent-based Approach", in: C. Sierra (ed.), 26th
31 #     International Joint Conference on Artificial Intelligence (IJCAI),
32 #     AAAI, p. 374-380. DOI: 10.24963/ijcai.2017/53
33
34
35 # License:
36 # --------
37
38 #    Copyright 2016 INRA and Univ. Lille
39
40 #    Inter Deposit Digital Number: IDDN.FR.001.280043.000.R.P.2018.000.10000
41
42 #    Agence pour la Protection des Programmes,
43 #    54 rue de Paradis, 75010 Paris, France
44
45 #    Licensed under the Apache License, Version 2.0 (the "License");
46 #    you may not use this file except in compliance with the License.
47 #    You may obtain a copy of the License at
48
49 #        http://www.apache.org/licenses/LICENSE-2.0
50
51 #    Unless required by applicable law or agreed to in writing, software
52 #    distributed under the License is distributed on an "AS IS" BASIS,
53 #    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
54 #    See the License for the specific language governing permissions and
55 #    limitations under the License.
57 from   abc                 import abstractmethod
58 import numpy               as     np
60 from   emulsion.tools.misc import retrieve_value, rates_to_probabilities
63 #  ______                    _   _
64 # |  ____|                  | | (_)
65 # | |__  __  _____ ___ _ __ | |_ _  ___  _ __  ___
66 # |  __| \ \/ / __/ _ \ '_ \| __| |/ _ \| '_ \/ __|
67 # | |____ >  < (_|  __/ |_) | |_| | (_) | | | \__ \
68 # |______/_/\_\___\___| .__/ \__|_|\___/|_| |_|___/
69 #                     | |
70 #                     |_|
72 class InvalidActionException(Exception):
73     """Exception raised when a semantic error occurs in action definition.
75     """
76     def __init__(self, message):
77         super().__init__()
78         self.message = message
80     def __str__(self):
81         return self.message
84 #   _____ _
85 #  / ____| |
86 # | |    | | __ _ ___ ___  ___  ___
87 # | |    | |/ _` / __/ __|/ _ \/ __|
88 # | |____| | (_| \__ \__ \  __/\__ \
89 #  \_____|_|\__,_|___/___/\___||___/
92 class AbstractAction(object):
93     """AbstractActions are aimed at describing actions triggered by a
94     state machine.
96     """
97     def __init__(self, state_machine=None, **_):
98         self.state_machine = state_machine
100     @abstractmethod
101     def execute_action(self, unit, **others):
102         """Execute the action on the specified unit."""
103 #        print(self, 'executed by', unit)
104         pass
106     @classmethod
107     def build_action(cls, action_name, **others):
108         """Return an instance of the appropriate Action subclass,
109         depending on its name. The appropriate parameters for this
110         action should be passed as a dictionary.
112         """
113         return ACTION_DICT[action_name](**others)
115     def __str__(self):
116         return self.__class__.__name__
118 class ValueAction(AbstractAction):
119     """ValueActions represent modifications of state variables or
120     attributes.
122     """
123     def __init__(self, statevar_name=None, parameter=None, delta_t=1, **others):
124         """Create a ValueAction aimed at modifying the specified
125         statevar according to the parameter.
127         """
128         super().__init__(**others)
129         self.statevar_name = statevar_name
130         self.parameter = parameter
131         self.delta_t = delta_t
133 class SetVarAction(ValueAction):
134     """SetVarAction allow to set the variable of the agent.
136     """
137     def __init__(self, statevar_name=None, parameter=None, model=None, **others):
138         """Create a SetVarAction aimed at modifying the specified statevar
139         according to the paramter.
141         """
142         super().__init__(**others)
143         if statevar_name in model.state_machines:
144             raise InvalidActionException("Action set_var must not change values of state machines. Use action become instead.\n\tset_var: {} value: {}".format(statevar_name, parameter))
145         self.statevar_name = statevar_name
146         self.parameter = parameter
147         # print(self)
149     def execute_action(self, unit, agents=None, **others):
150         """Execute the action in the specified unit. If the `agents` parameter
151         is specified (as a list), each agent of this list will execute
152         the action. If changes of state variables in relation to a
153         state machine occur, the corresponding actions (if any) are
154         executed: on_exit from the current state, and on_enter for the
155         new state.
157         """
158         if agents is None:
159             agents = [unit]
160         for agent in agents:
161             value = agent.get_model_value(self.parameter)
162             # agent.set_information(self.statevar_name, value)
163             agent.statevars[self.statevar_name] = value
165     def __str__(self):
166         return super().__str__() + ' {} <- {}'.format(self.statevar_name,
167                                                       self.parameter)
168     __repr__ = __str__
171 class RateAdditiveAction(ValueAction):
172     """A RateChangeAction is aimed at increasing or decreasing a
173     specific state variable or attribute, according to a specific rate
174     (i.e. the actual increase or decrease is the product of the
175     `parameter` attribute and a population size).
177     """
178     def __init__(self, sign=1, **others):
179         super().__init__(**others)
180         self.sign = sign
182     def execute_action(self, unit, population=None, agents=None):
183         """Execute the action on the specified unit, with the
184         specified population size.
186         """
187         super().execute_action(unit)
188         if population is None:
189             population = len(agents)
190         rate_value = self.state_machine.get_value(self.parameter)
191         rate = retrieve_value(rate_value, unit)
192         current_val = unit.get_information(self.statevar_name)
193         new_val = current_val + self.sign*rate*population*self.delta_t
194         # print('Executing', self.__class__.__name__, 'for', unit,
195         #       self.statevar_name, current_val, '->', new_val,
196         #       self.sign, rate, population)
197         unit.set_information(self.statevar_name, new_val)
199     def __str__(self):
200         return super().__str__() + ' ({}, {})'.format(self.statevar_name,
201                                                       self.parameter)
202     __repr__ = __str__
205 class RateDecreaseAction(RateAdditiveAction):
206     """A RateDecreaseAction is aimed at decreasing a specific state
207     variable or attribute, according to a specific rate (i.e. the
208     actual decrease is the product of the `parameter` attribute and a
209     population size).
211     """
212     def __init__(self, **others):
213         super().__init__(sign=-1, **others)
215 class RateIncreaseAction(RateAdditiveAction):
216     """A RateIncreaseAction is aimed at increasing a specific state
217     variable or attribute, according to a specific rate (i.e. the
218     actual increase is the product of the `parameter` attribute and a
219     population size).
221     """
222     def __init__(self, **others):
223         super().__init__(sign=1, **others)
225 class StochAdditiveAction(ValueAction):
226     """A StochAdditiveAction is aimed at increasing or decreasing a
227     specific state variable or attribute, according to a specific
228     rate, using a *binomial sampling*.
230     """
231     def __init__(self, sign=1, **others):
232         super().__init__(**others)
233         self.sign = sign
235     def execute_action(self, unit, population=None, agents=None):
236         """Execute the action on the specified unit, with the
237         specified population size.
239         """
240         super().execute_action(unit)
241         if population is None:
242             population = len(agents)
243         rate_value = self.state_machine.get_value(self.parameter)
244         rate = retrieve_value(rate_value, unit)
245         # convert rate into a probability
246         proba = rates_to_probabilities(rate, [rate], delta_t=self.delta_t)[0]
247         current_val = unit.get_information(self.statevar_name)
248         new_val = current_val + self.sign*np.random.binomial(population, proba)
249         # print('Executing', self.__class__.__name__, 'for', unit,
250         #       self.statevar_name, current_val, '->', new_val,
251         #       self.sign, rate, population)
252         unit.set_information(self.statevar_name, new_val)
254     def __str__(self):
255         return super().__str__() + ' ({}, {})'.format(self.statevar_name,
256                                                       self.parameter)
257     __repr__ = __str__
260 class StochDecreaseAction(StochAdditiveAction):
261     """A StochDecreaseAction is aimed at decreasing a specific state
262     variable or attribute, according to a specific rate, using a
263     *binomial sampling*.
265     """
266     def __init__(self, **others):
267         super().__init__(sign=-1, **others)
269 class StochIncreaseAction(StochAdditiveAction):
270     """A StochIncreaseAction is aimed at increasing a specific state
271     variable or attribute, according to a specific rate, using a
272     *binomial sampling*.
274     """
275     def __init__(self, **others):
276         super().__init__(sign=1, **others)
278 class StringAction(AbstractAction):
279     """A StringAction is based on the specification of a string
280     parameter.
282     """
283     def __init__(self, parameter=None, l_params=[], d_params={}, **others):
284         super().__init__(**others)
285         self.parameter = parameter
286         self.l_params = l_params
287         self.d_params = d_params
289     def __str__(self):
290         return super().__str__() + ' ({!s}, {}, {})'.format(self.parameter,
291                                                             self.l_params,
292                                                             self.d_params)
293     __repr__ = __str__
296 class BecomeAction(StringAction):
297     """A BecomeAction is aimed at making an agent change its state
298     according to a specified prototype an action. It requires a
299     prototype name.
301     """
302     def execute_action(self, unit, agents=None, **others):
303         """Execute the action in the specified unit. If the `agents` parameter
304         is specified (as a list), each agent of this list will execute
305         the action. If changes of state variables in relation to a
306         state machine occur, the corresponding actions (if any) are
307         executed: on_exit from the current state, and on_enter for the
308         new state.
310         """
311         if agents is None:
312             agents = [unit]
313         for agent in agents:
314             agent.apply_prototype(name=self.parameter, execute_actions=True)
316 class CloneAction(AbstractAction):
317     """A CloneAction produces several copies of the agent with a given
318     prototype.
320     """
321     def __init__(self, prototypes=[], amount=None, probas=None, model=None, **others):
322         super().__init__(**others)
323         self.prototype_names = prototypes if isinstance(prototypes, list)\
324                                else [prototypes]
325         amount= amount if amount is not None else 1
326         if probas is None:
327             probas = [1/len(self.prototype_names)] * len(self.prototype_names)
328         else:
329             probas = probas if isinstance(probas, list) else [probas]
330         assert(len(self.prototype_names) - len(probas) <= 1)
331         self.amount = model.add_expression(amount)
332         self.probas = [model.add_expression(pr) for pr in probas]
334     def __str__(self):
335         return super().__str__() + ' ({!s}, {}, {})'.format(self.prototype_names,
336                                                             self.amount, self.probas)
337     __repr__ = __str__
339     def execute_action(self, unit, agents=None, **others):
340         """Execute the action in the specified unit. If the `agents` parameter
341         is specified (as a list), each agent of this list will execute
342         the action. If changes of state variables in relation to a
343         state machine occur, the corresponding actions (if any) are
344         executed: on_exit from the current state, and on_enter for the
345         new state.
347         """
348         if agents is None:
349             agents = [unit]
350         for agent in agents:
351             protos = list(self.prototype_names)
352             # compute actual values for probabilities
353             proba_values = [agent.get_model_value(prob) for prob in self.probas]
354             total = sum(proba_values)
355             assert(0 <= total <= 1)
356             if total < 1:
357                 # add complement
358                 proba_values.append(1 - total)
359                 # if N-1 probabilities were given for N prototypes, no
360                 # problem: the last prototype is getting the 1-total
361                 # value. Otherwise, this means that there is a
362                 # possibility that no individuals are produced => None
363                 if len(proba_values) > len(protos):
364                     protos.append(None)
365             quantities = np.random.multinomial(int(agent.get_model_value(self.amount)),
366                                                proba_values)
367             for prototype, quantity in zip(protos, quantities):
368                 if prototype is not None:
369                     newborns = [agent.clone(prototype = prototype)
370                                 for _ in range(quantity)]
371                     agent.upper_level().add_atoms(newborns)
374 class MessageAction(StringAction):
375     """A MessageAction is aimed at making an agent print a given
376     string. It requires a string message. This string can contain one
377     reference to a variable or method of the agent, using Python's
378     formatting syntax.
380     For instance, 'My state is {.statevars.health_state}' will print
381     the current health state of the agent.
383     Output is formatted in three comma-separated fields: the time step
384     when the message was produced, the agent speaking, and the message
385     itself.
387     """
388     def execute_action(self, unit, agents=None, **others):
389         """Execute the action in the specified unit. If the `agents` parameter
390         is specified (as a list), each agent of this list will execute
391         the action.
393         """
394         if agents is None:
395             agents = [unit]
396         for agent in agents:
397             message = self.parameter.format(agent)
398             print("@{}, {}, {}".format(agent.statevars.step, agent, message))
401 class MethodAction(AbstractAction):
402     """A MethodAction is aimed at making an agent perform an action on
403     a specific population. It requires a method name, and optionnally
404     a list and a dictionary of parameters.
406     """
407     def __init__(self, method=None, l_params=[], d_params={}, **others):
408         super().__init__(**others)
409         self.method = method
410         self.l_params = l_params
411         self.d_params = d_params
413     def __str__(self):
414         return super().__str__() + ' ({!s}, {}, {})'.format(self.method,
415                                                             self.l_params,
416                                                             self.d_params)
417     __repr__ = __str__
419     def execute_action(self, unit, agents=None, **others):
420         """Execute the action using the specified unit. If the
421         `agents` parameter is a list of units, each unit of this list
422         will execute the action.
424         """
425         if agents is None:
426             agents = [unit]
427         for agent in agents:
428             action = getattr(agent, self.method)
429             l_params = [retrieve_value(self.state_machine.get_value(expr), agent)
430                         for expr in self.l_params]
431             ### introduced to pass internal information such as population
432             d_params = others
433             d_params.update({key: retrieve_value(self.state_machine.get_value(expr), agent)
434                              for key, expr in self.d_params.items()})
435             action(*l_params, **d_params)
437 class FunctionAction(MethodAction):
438     """A FunctionAction is aimed at making an agent perform an action
439     on a specific population. It requires a function, and optionnally
440     a list and a dictionary of parameters. A FunctionAction runs
441     faster than a MethodAction since it does not require to retrieve
442     the method in each agent.
444     """
445     def __init__(self, function=None, **others):
446         super().__init__(**others)
447         self.function = function
448         self.method = function.__name__
450     def execute_action(self, unit, agents=None, **others):
451         """Execute the action using the specified unit. If the
452         `agents` parameter is a list of units, each unit of this list
453         will execute the action.
455         """
456         if agents is None:
457             agents = [unit]
458         for agent in agents:
459             l_params = [retrieve_value(self.state_machine.get_value(expr),
460                                        agent)
461                         for expr in self.l_params]
462             ### introduced to pass internal information such as population
463             d_params = others
464             d_params.update({key:\
465                              retrieve_value(self.state_machine.get_value(expr),
466                                             agent)
467                              for key, expr in self.d_params.items()})
468             self.function(agent, *l_params, **d_params)
471 ACTION_DICT = {
472     'increase': RateIncreaseAction,
473     'decrease': RateDecreaseAction,
474     'increase_stoch': StochIncreaseAction,
475     'decrease_stoch': StochDecreaseAction,
476     'message': MessageAction,
477     'become': BecomeAction,
478     'clone': CloneAction,
479     'produce_offspring': CloneAction,
480     'action': MethodAction,
481     'duration': FunctionAction,
482     'set_var': SetVarAction