simulation.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. # coding: utf-8
  2. import typing
  3. import time
  4. from synergine2.base import BaseObject
  5. from synergine2.base import IdentifiedObject
  6. from synergine2.config import Config
  7. from synergine2.exceptions import ConfigurationError
  8. from synergine2.share import shared
  9. from synergine2.utils import get_mechanisms_classes
  10. class Intention(object):
  11. pass
  12. class IntentionManager(object):
  13. intentions = shared.create_self('intentions', lambda: {}) # type: typing.Dict[typing.Type[Intention], Intention]
  14. def __init__(self):
  15. self._id = id(self)
  16. @property
  17. def id(self) -> int:
  18. return self._id
  19. def set(self, intention: Intention) -> None:
  20. self.intentions[type(intention)] = intention
  21. def get(self, intention_type: typing.Type[Intention]) -> Intention:
  22. # TODO: Raise specialised exception if KeyError
  23. return self.intentions[intention_type]
  24. def remove(self, intention_type: typing.Type[Intention]) -> None:
  25. intentions = self.intentions
  26. del intentions[intention_type]
  27. self.intentions = intentions
  28. def remove_all(self) -> None:
  29. self.intentions = {}
  30. class Subject(IdentifiedObject):
  31. start_collections = []
  32. behaviours_classes = []
  33. behaviour_selector_class = None # type: typing.Type[SubjectBehaviourSelector]
  34. intention_manager_class = None # type: typing.Type[IntentionManager]
  35. collections = shared.create_self('collections', lambda: [])
  36. def __init__(
  37. self,
  38. config: Config,
  39. simulation: 'Simulation',
  40. properties: dict=None,
  41. ):
  42. """
  43. :param config: config object
  44. :param simulation: simulation object
  45. :param properties: additional data (will not change during simulation)
  46. """
  47. super().__init__()
  48. # FIXME: use shared data to permit dynamic start_collections
  49. self.collections.extend(self.start_collections[:])
  50. self.config = config
  51. self._id = id(self) # We store object id because it's lost between process
  52. self.simulation = simulation
  53. self.intentions = None
  54. self.properties = properties or {}
  55. if self.behaviour_selector_class:
  56. self.behaviour_selector = self.behaviour_selector_class()
  57. else:
  58. self.behaviour_selector = SubjectBehaviourSelector()
  59. if self.intention_manager_class:
  60. self.intentions = self.intention_manager_class()
  61. else:
  62. self.intentions = IntentionManager()
  63. self._mechanisms = None # type: typing.Dict[typing.Type['SubjectMechanism'], 'SubjectMechanism']
  64. self._behaviours = None # type: typing.Dict[typing.Type['SubjectBehaviour'], 'SubjectBehaviour']
  65. @property
  66. def mechanisms(self) -> typing.Dict[typing.Type['SubjectMechanism'], 'SubjectMechanism']:
  67. if self._mechanisms is None:
  68. self._mechanisms = {}
  69. for behaviour_class in self.behaviours_classes:
  70. for mechanism_class in behaviour_class.use:
  71. mechanism = mechanism_class(
  72. self.config,
  73. self.simulation,
  74. self,
  75. )
  76. self._mechanisms[mechanism_class] = mechanism
  77. return self._mechanisms
  78. @property
  79. def behaviours(self) -> typing.Dict[typing.Type['SubjectBehaviour'], 'SubjectBehaviour']:
  80. if self._behaviours is None:
  81. self._behaviours = {}
  82. for behaviour_class in self.behaviours_classes:
  83. behaviour = behaviour_class(
  84. self.config,
  85. self.simulation,
  86. self,
  87. )
  88. self._behaviours[behaviour_class] = behaviour
  89. return self._behaviours
  90. def change_id(self, id_: int) -> None:
  91. self._id = id_
  92. def expose(self) -> None:
  93. subject_classes = shared.get('subject_classes')
  94. subject_classes[self._id] = type(self)
  95. shared.set('subject_classes', subject_classes)
  96. for collection in self.collections:
  97. self.simulation.collections.setdefault(collection, []).append(self.id)
  98. def remove_collection(self, collection_name: str) -> None:
  99. self.collections.remove(collection_name)
  100. # Manipulate as shared property
  101. simulation_collection = self.simulation.collections[collection_name]
  102. simulation_collection.remove(self.id)
  103. self.simulation.collections[collection_name] = simulation_collection
  104. def __str__(self):
  105. return self.__repr__()
  106. def __repr__(self):
  107. return '{}({})'.format(
  108. type(self).__name__,
  109. self.id,
  110. )
  111. class Subjects(list):
  112. """
  113. TODO: Manage other list methods
  114. """
  115. subject_ids = shared.create('subject_ids', [])
  116. def __init__(self, *args, **kwargs):
  117. self.simulation = kwargs.pop('simulation')
  118. self.removes = []
  119. self.adds = []
  120. self.track_changes = False
  121. self.index = {}
  122. self._auto_expose = True
  123. super().__init__(*args, **kwargs)
  124. if self.auto_expose:
  125. for subject in self:
  126. subject.expose()
  127. @property
  128. def auto_expose(self) -> bool:
  129. return self._auto_expose
  130. @auto_expose.setter
  131. def auto_expose(self, value: bool) -> None:
  132. assert self._auto_expose
  133. self._auto_expose = value
  134. def remove(self, value: Subject):
  135. # Remove from index
  136. del self.index[value.id]
  137. self.subject_ids.remove(value.id)
  138. # Remove from subjects list
  139. super().remove(value)
  140. # Remove from start_collections
  141. for collection_name in value.collections:
  142. self.simulation.collections[collection_name].remove(value.id)
  143. # Add to removed listing
  144. if self.track_changes:
  145. self.removes.append(value)
  146. # TODO: Supprimer des choses du shared ! Sinon fuite mémoire dans la bdd
  147. def append(self, p_object):
  148. # Add to index
  149. self.index[p_object.id] = p_object
  150. self.subject_ids.append(p_object.id)
  151. # Add to subjects list
  152. super().append(p_object)
  153. # Add to adds list
  154. if self.track_changes:
  155. self.adds.append(p_object)
  156. if self.auto_expose:
  157. p_object.expose()
  158. def extend(self, iterable):
  159. for item in iterable:
  160. self.append(item)
  161. class Simulation(BaseObject):
  162. accepted_subject_class = Subjects
  163. behaviours_classes = [] # type: typing.List[typing.Type[SimulationBehaviour]]
  164. subject_classes = shared.create('subject_classes', {})
  165. collections = shared.create('collections', {})
  166. def __init__(
  167. self,
  168. config: Config,
  169. ):
  170. self.config = config
  171. self._subjects = None # type: Subjects
  172. self._index_locked = False
  173. self.behaviours = {}
  174. self.mechanisms = {}
  175. for mechanism_class in get_mechanisms_classes(self):
  176. self.mechanisms[mechanism_class] = mechanism_class(
  177. config=self.config,
  178. simulation=self,
  179. )
  180. for behaviour_class in self.behaviours_classes:
  181. self.behaviours[behaviour_class] = behaviour_class(
  182. config=self.config,
  183. simulation=self,
  184. )
  185. def lock_index(self) -> None:
  186. self._index_locked = True
  187. @property
  188. def subjects(self):
  189. return self._subjects
  190. @subjects.setter
  191. def subjects(self, value: 'Subjects'):
  192. if not isinstance(value, self.accepted_subject_class):
  193. raise Exception('Simulation.subjects must be {0} type'.format(
  194. self.accepted_subject_class,
  195. ))
  196. self._subjects = value
  197. def get_or_create_subject(self, subject_id: int) -> Subject:
  198. try:
  199. return self._subjects.index[subject_id]
  200. except KeyError:
  201. # We should be in process context and subject have to been created
  202. subject_class = shared.get('subject_classes')[subject_id]
  203. subject = subject_class(self.config, self)
  204. subject.change_id(subject_id)
  205. self.subjects.append(subject)
  206. return subject
  207. class Mechanism(BaseObject):
  208. pass
  209. class SubjectMechanism(Mechanism):
  210. def __init__(
  211. self,
  212. config: Config,
  213. simulation: Simulation,
  214. subject: Subject,
  215. ):
  216. self.config = config
  217. self.simulation = simulation
  218. self.subject = subject
  219. def run(self) -> dict:
  220. raise NotImplementedError()
  221. class SimulationMechanism(Mechanism):
  222. """If parallelizable behaviour, call """
  223. parallelizable = False
  224. def __init__(
  225. self,
  226. config: Config,
  227. simulation: Simulation,
  228. ):
  229. self.config = config
  230. self.simulation = simulation
  231. def repr_debug(self) -> str:
  232. return self.__class__.__name__
  233. def run(self, process_number: int=None, process_count: int=None):
  234. raise NotImplementedError()
  235. class Event(BaseObject):
  236. def repr_debug(self) -> str:
  237. return self.__dict__
  238. class Behaviour(BaseObject):
  239. def __init__(self):
  240. self.last_execution_time = 0
  241. @property
  242. def cycle_frequency(self) -> typing.Optional[float]:
  243. return None
  244. @property
  245. def seconds_frequency(self) -> typing.Optional[float]:
  246. """
  247. If this behaviour is time based, return here the waiting time between two
  248. executions. IMPORTANT: your behaviour have to update it's
  249. self.last_execution_time attribute when executed (in `run` method)!
  250. :return: float number of period in seconds
  251. """
  252. return None
  253. def run(self, data):
  254. raise NotImplementedError()
  255. def is_skip(self, cycle_number: int) -> bool:
  256. """
  257. :return: True if behaviour have to be skip this time
  258. """
  259. cycle_frequency = self.cycle_frequency
  260. if cycle_frequency is not None:
  261. return bool(cycle_number % cycle_frequency)
  262. seconds_frequency = self.seconds_frequency
  263. if seconds_frequency is not None:
  264. return float(time.time() - self.last_execution_time) <= seconds_frequency
  265. return False
  266. class SubjectBehaviour(Behaviour):
  267. use = [] # type: typing.List[typing.Type[SubjectMechanism]]
  268. def __init__(
  269. self,
  270. config: Config,
  271. simulation: Simulation,
  272. subject: Subject,
  273. ):
  274. super().__init__()
  275. self.config = config
  276. self.simulation = simulation
  277. self.subject = subject
  278. def is_terminated(self) -> bool:
  279. """
  280. :return: True if behaviour will no longer exist (can be removed from simulation)
  281. """
  282. return False
  283. def run(self, data) -> object:
  284. """
  285. Method called in subprocess.
  286. If return equivalent to False, behaviour produce nothing.
  287. If return equivalent to True, action bahaviour method
  288. will be called with these data
  289. Note: Returned data will be transfered from sub processes.
  290. Prefer scalar types.
  291. """
  292. raise NotImplementedError() # TODO Test it and change to strictly False
  293. def action(self, data) -> [Event]:
  294. """
  295. Method called in main process
  296. Return events will be give to terminals
  297. """
  298. raise NotImplementedError()
  299. class SimulationBehaviour(Behaviour):
  300. use = [] # type: typing.List[typing.Type[SimulationMechanism]]
  301. def __init__(
  302. self,
  303. config: Config,
  304. simulation: Simulation,
  305. ):
  306. super().__init__()
  307. self.config = config
  308. self.simulation = simulation
  309. def run(self, data):
  310. """
  311. Method called in subprocess if mechanisms are
  312. parallelizable, in main process if not.
  313. """
  314. raise NotImplementedError()
  315. @classmethod
  316. def merge_data(cls, new_data, start_data=None):
  317. """
  318. Called if behaviour executed in subprocess
  319. """
  320. raise NotImplementedError()
  321. def action(self, data) -> typing.List[Event]:
  322. """
  323. Method called in main process
  324. Return events will be give to terminals
  325. """
  326. raise NotImplementedError()
  327. class SubjectBehaviourSelector(BaseObject):
  328. def reduce_behaviours(
  329. self,
  330. behaviours: typing.Dict[typing.Type[SubjectBehaviour], object],
  331. ) -> typing.Dict[typing.Type[SubjectBehaviour], object]:
  332. return behaviours
  333. class BehaviourStep(object):
  334. def proceed(self) -> 'BehaviourStep':
  335. raise NotImplementedError()
  336. def generate_data(self) -> typing.Any:
  337. raise NotImplementedError()
  338. def get_events(self) -> typing.List[Event]:
  339. raise NotImplementedError()
  340. class SubjectComposedBehaviour(SubjectBehaviour):
  341. """
  342. SubjectComposedBehaviour receive data in run (and will will it's step with it).
  343. These data can be the first data of behaviour, or last behaviour subject data
  344. produced in action.
  345. SubjectComposedBehaviour produce data in run only if something happen and must be
  346. given in future run.
  347. """
  348. step_classes = None # type: typing.List[typing.Tuple[typing.Type[BehaviourStep], typing.Callable[[typing.Any], bool]]] # nopep8
  349. def __init__(
  350. self,
  351. config: Config,
  352. simulation: Simulation,
  353. subject: Subject,
  354. ) -> None:
  355. super().__init__(config, simulation, subject)
  356. if self.step_classes is None:
  357. raise ConfigurationError(
  358. '{}: you must set step_classes class attribute'.format(
  359. self.__class__.__name__,
  360. ),
  361. )
  362. def get_step(self, data) -> BehaviourStep:
  363. for step_class, step_test_callable in self.step_classes:
  364. if step_test_callable(data):
  365. return step_class(**data)
  366. raise ConfigurationError(
  367. '{}: No step choose for following data: {}'.format(
  368. self.__class__.__name__,
  369. data,
  370. ),
  371. )
  372. def run(self, data):
  373. step = self.get_step(data)
  374. next_step = step.proceed()
  375. return next_step.generate_data()
  376. def action(self, data):
  377. step = self.get_step(data)
  378. return step.get_events()
  379. def disable_when(
  380. predicate: typing.Callable[[Config, Simulation], bool],
  381. ):
  382. def decorator(func) -> typing.Any:
  383. def wrapper(self, *args, **kwargs):
  384. if predicate(self.config, self.simulation):
  385. return False
  386. return func(self, *args, **kwargs)
  387. return wrapper
  388. return decorator
  389. def config_value(parameter_to_resolve: str):
  390. def predicate(
  391. config: Config,
  392. simulation: Simulation,
  393. ):
  394. return bool(config.resolve(parameter_to_resolve, False))
  395. return predicate