simulation.py 12KB

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