simulation.py 11KB

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