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

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             # if the value which has to be affected to the variable is
162             # already a statevar, retrieve its value from the agent
163             if self.parameter in agent.statevars:
164                 value = agent.statevars[self.parameter]
165             # otherwise get its value through the model
166             else:
167                 value = agent.get_model_value(self.parameter)
168             # agent.set_information(self.statevar_name, value)
169             agent.statevars[self.statevar_name] = value
171     def __str__(self):
172         return super().__str__() + ' {} <- {}'.format(self.statevar_name,
173                                                       self.parameter)
174     __repr__ = __str__
177 class RateAdditiveAction(ValueAction):
178     """A RateChangeAction is aimed at increasing or decreasing a
179     specific state variable or attribute, according to a specific rate
180     (i.e. the actual increase or decrease is the product of the
181     `parameter` attribute and a population size).
183     """
184     def __init__(self, sign=1, **others):
185         super().__init__(**others)
186         self.sign = sign
188     def execute_action(self, unit, population=None, agents=None):
189         """Execute the action on the specified unit, with the
190         specified population size.
192         """
193         super().execute_action(unit)
194         if population is None:
195             population = len(agents)
196         rate_value = self.state_machine.get_value(self.parameter)
197         rate = retrieve_value(rate_value, unit)
198         current_val = unit.get_information(self.statevar_name)
199         new_val = current_val + self.sign*rate*population*self.delta_t
200         # print('Executing', self.__class__.__name__, 'for', unit,
201         #       self.statevar_name, current_val, '->', new_val,
202         #       self.sign, rate, population)
203         unit.set_information(self.statevar_name, new_val)
205     def __str__(self):
206         return super().__str__() + ' ({}, {})'.format(self.statevar_name,
207                                                       self.parameter)
208     __repr__ = __str__
211 class RateDecreaseAction(RateAdditiveAction):
212     """A RateDecreaseAction is aimed at decreasing a specific state
213     variable or attribute, according to a specific rate (i.e. the
214     actual decrease is the product of the `parameter` attribute and a
215     population size).
217     """
218     def __init__(self, **others):
219         super().__init__(sign=-1, **others)
221 class RateIncreaseAction(RateAdditiveAction):
222     """A RateIncreaseAction is aimed at increasing a specific state
223     variable or attribute, according to a specific rate (i.e. the
224     actual increase is the product of the `parameter` attribute and a
225     population size).
227     """
228     def __init__(self, **others):
229         super().__init__(sign=1, **others)
231 class StochAdditiveAction(ValueAction):
232     """A StochAdditiveAction is aimed at increasing or decreasing a
233     specific state variable or attribute, according to a specific
234     rate, using a *binomial sampling*.
236     """
237     def __init__(self, sign=1, **others):
238         super().__init__(**others)
239         self.sign = sign
241     def execute_action(self, unit, population=None, agents=None):
242         """Execute the action on the specified unit, with the
243         specified population size.
245         """
246         super().execute_action(unit)
247         if population is None:
248             population = len(agents)
249         rate_value = self.state_machine.get_value(self.parameter)
250         rate = retrieve_value(rate_value, unit)
251         # convert rate into a probability
252         proba = rates_to_probabilities(rate, [rate], delta_t=self.delta_t)[0]
253         current_val = unit.get_information(self.statevar_name)
254         new_val = current_val + self.sign*np.random.binomial(population, proba)
255         # print('Executing', self.__class__.__name__, 'for', unit,
256         #       self.statevar_name, current_val, '->', new_val,
257         #       self.sign, rate, population)
258         unit.set_information(self.statevar_name, new_val)
260     def __str__(self):
261         return super().__str__() + ' ({}, {})'.format(self.statevar_name,
262                                                       self.parameter)
263     __repr__ = __str__
266 class StochDecreaseAction(StochAdditiveAction):
267     """A StochDecreaseAction is aimed at decreasing a specific state
268     variable or attribute, according to a specific rate, using a
269     *binomial sampling*.
271     """
272     def __init__(self, **others):
273         super().__init__(sign=-1, **others)
275 class StochIncreaseAction(StochAdditiveAction):
276     """A StochIncreaseAction is aimed at increasing a specific state
277     variable or attribute, according to a specific rate, using a
278     *binomial sampling*.
280     """
281     def __init__(self, **others):
282         super().__init__(sign=1, **others)
284 class StringAction(AbstractAction):
285     """A StringAction is based on the specification of a string
286     parameter.
288     """
289     def __init__(self, parameter=None, l_params=[], d_params={}, **others):
290         super().__init__(**others)
291         self.parameter = parameter
292         self.l_params = l_params
293         self.d_params = d_params
295     def __str__(self):
296         return super().__str__() + ' ({!s}, {}, {})'.format(self.parameter,
297                                                             self.l_params,
298                                                             self.d_params)
299     __repr__ = __str__
302 class BecomeAction(AbstractAction):
303     """A BecomeAction is aimed at making an agent change its state
304     according to one ore more specified prototypes.
306     """
307     def __init__(self, prototypes=[], probas=None, model=None, **others):
308         super().__init__(**others)
309         self.prototype_names = prototypes if isinstance(prototypes, list)\
310                                else [prototypes]
311         if probas is None:
312             probas = [1/len(self.prototype_names)] * len(self.prototype_names)
313         else:
314             probas = probas if isinstance(probas, list) else [probas]
315         assert(len(self.prototype_names) - len(probas) <= 1)
316         self.probas = [model.add_expression(pr) for pr in probas]
318     def __str__(self):
319         return super().__str__() + ' ({!s}, {})'.format(self.prototype_names, self.probas)
320     __repr__ = __str__
322     def execute_action(self, unit, agents=None, **others):
323         """Execute the action in the specified unit. If the `agents` parameter
324         is specified (as a list), each agent of this list will execute
325         the action. If changes of state variables in relation to a
326         state machine occur, the corresponding actions (if any) are
327         executed: on_exit from the current state, and on_enter for the
328         new state.
330         """
331         if agents is None:
332             agents = [unit]
333         for agent in agents:
334             protos = list(self.prototype_names)
335             # compute actual values for probabilities
336             proba_values = [agent.get_model_value(prob) for prob in self.probas]
337             total = sum(proba_values)
338             assert(0 <= total <= 1)
339             if total < 1:
340                 # add complement
341                 proba_values.append(1 - total)
342                 # if N-1 probabilities were given for N prototypes, no
343                 # problem: the last prototype is getting the 1-total
344                 # value. Otherwise, this means that there is a
345                 # possibility that no individuals are produced => None
346                 if len(proba_values) > len(protos):
347                     protos.append(None)
348             prototype = np.random.choice(protos, p=proba_values)
349             if prototype is not None:
350                 agent.apply_prototype(name=prototype, execute_actions=True)
352 class CloneAction(AbstractAction):
353     """A CloneAction produces several copies of the agent with a given
354     prototype.
356     """
357     def __init__(self, prototypes=[], amount=None, probas=None, model=None, **others):
358         super().__init__(**others)
359         self.prototype_names = prototypes if isinstance(prototypes, list)\
360                                else [prototypes]
361         amount= amount if amount is not None else 1
362         if probas is None:
363             probas = [1/len(self.prototype_names)] * len(self.prototype_names)
364         else:
365             probas = probas if isinstance(probas, list) else [probas]
366         assert(len(self.prototype_names) - len(probas) <= 1)
367         self.amount = model.add_expression(amount)
368         self.probas = [model.add_expression(pr) for pr in probas]
370     def __str__(self):
371         return super().__str__() + ' ({!s}, {}, {})'.format(self.prototype_names,
372                                                             self.amount, self.probas)
373     __repr__ = __str__
375     def execute_action(self, unit, agents=None, **others):
376         """Execute the action in the specified unit. If the `agents` parameter
377         is specified (as a list), each agent of this list will execute
378         the action. If changes of state variables in relation to a
379         state machine occur, the corresponding actions (if any) are
380         executed: on_exit from the current state, and on_enter for the
381         new state.
383         """
384         if agents is None:
385             agents = [unit]
386         for agent in agents:
387             protos = list(self.prototype_names)
388             # compute actual values for probabilities
389             proba_values = [agent.get_model_value(prob) for prob in self.probas]
390             total = sum(proba_values)
391             assert(0 <= total <= 1)
392             if total < 1:
393                 # add complement
394                 proba_values.append(1 - total)
395                 # if N-1 probabilities were given for N prototypes, no
396                 # problem: the last prototype is getting the 1-total
397                 # value. Otherwise, this means that there is a
398                 # possibility that no individuals are produced => None
399                 if len(proba_values) > len(protos):
400                     protos.append(None)
401             quantities = np.random.multinomial(int(agent.get_model_value(self.amount)),
402                                                proba_values)
403             for prototype, quantity in zip(protos, quantities):
404                 if prototype is not None:
405                     newborns = [agent.clone(prototype = prototype)
406                                 for _ in range(quantity)]
407                     agent.upper_level().add_atoms(newborns)
410 class MessageAction(StringAction):
411     """A MessageAction is aimed at making an agent print a given
412     string. It requires a string message. This string can contain one
413     reference to a variable or method of the agent, using Python's
414     formatting syntax.
416     For instance, 'My state is {.statevars.health_state}' will print
417     the current health state of the agent.
419     Output is formatted in three comma-separated fields: the time step
420     when the message was produced, the agent speaking, and the message
421     itself.
423     """
424     def execute_action(self, unit, agents=None, **others):
425         """Execute the action in the specified unit. If the `agents` parameter
426         is specified (as a list), each agent of this list will execute
427         the action.
429         """
430         if agents is None:
431             agents = [unit]
432         for agent in agents:
433             message = self.parameter.format(agent)
434             print("@{}, {}, {}".format(agent.statevars.step, agent, message))
437 class MethodAction(AbstractAction):
438     """A MethodAction is aimed at making an agent perform an action on
439     a specific population. It requires a method name, and optionnally
440     a list and a dictionary of parameters.
442     """
443     def __init__(self, method=None, l_params=[], d_params={}, **others):
444         super().__init__(**others)
445         self.method = method
446         self.l_params = l_params
447         self.d_params = d_params
449     def __str__(self):
450         return super().__str__() + ' ({!s}, {}, {})'.format(self.method,
451                                                             self.l_params,
452                                                             self.d_params)
453     __repr__ = __str__
455     def execute_action(self, unit, agents=None, **others):
456         """Execute the action using the specified unit. If the
457         `agents` parameter is a list of units, each unit of this list
458         will execute the action.
460         """
461         if agents is None:
462             agents = [unit]
463         for agent in agents:
464             action = getattr(agent, self.method)
465             l_params = [retrieve_value(self.state_machine.get_value(expr), agent)
466                         for expr in self.l_params]
467             ### introduced to pass internal information such as population
468             d_params = others
469             d_params.update({key: retrieve_value(self.state_machine.get_value(expr), agent)
470                              for key, expr in self.d_params.items()})
471             action(*l_params, **d_params)
473 class FunctionAction(MethodAction):
474     """A FunctionAction is aimed at making an agent perform an action
475     on a specific population. It requires a function, and optionnally
476     a list and a dictionary of parameters. A FunctionAction runs
477     faster than a MethodAction since it does not require to retrieve
478     the method in each agent.
480     """
481     def __init__(self, function=None, **others):
482         super().__init__(**others)
483         self.function = function
484         self.method = function.__name__
486     def execute_action(self, unit, agents=None, **others):
487         """Execute the action using the specified unit. If the
488         `agents` parameter is a list of units, each unit of this list
489         will execute the action.
491         """
492         if agents is None:
493             agents = [unit]
494         for agent in agents:
495             l_params = [retrieve_value(self.state_machine.get_value(expr),
496                                        agent)
497                         for expr in self.l_params]
498             ### introduced to pass internal information such as population
499             d_params = others
500             d_params.update({key:\
501                              retrieve_value(self.state_machine.get_value(expr),
502                                             agent)
503                              for key, expr in self.d_params.items()})
504             self.function(agent, *l_params, **d_params)
507 ACTION_DICT = {
508     'increase': RateIncreaseAction,
509     'decrease': RateDecreaseAction,
510     'increase_stoch': StochIncreaseAction,
511     'decrease_stoch': StochDecreaseAction,
512     'message': MessageAction,
513     'become': BecomeAction,
514     'clone': CloneAction,
515     'produce_offspring': CloneAction,
516     'action': MethodAction,
517     'duration': FunctionAction,
518     'set_var': SetVarAction