solver.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. # Copyright 2014 Modelling, Simulation and Design Lab (MSDL) at
  2. # McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """
  16. The actual DEVS solvers containing the main DEVS implementation
  17. """
  18. from collections import defaultdict
  19. from pypdevs.DEVS import *
  20. from pypdevs.util import *
  21. from pypdevs.logger import *
  22. from pypdevs.classicDEVSWrapper import ClassicDEVSWrapper
  23. class Solver(object):
  24. """
  25. A unified DEVS solver, containing all necessary functions
  26. """
  27. def __init__(self, listeners = {}):
  28. """
  29. Constructor
  30. """
  31. self.activities = {}
  32. self.dsdevs_dict = {}
  33. self.listeners = listeners
  34. def atomicOutputGenerationEventTracing(self, aDEVS, time):
  35. """
  36. Wrapper for the AtomicDEVS output function, which will save event counts
  37. :param aDEVS: the AtomicDEVS model that generates the output
  38. :param time: the time at which the output must be generated
  39. :returns: dict -- the generated output
  40. """
  41. retval = Solver.atomicOutputGeneration(self, aDEVS, time)
  42. for port in retval:
  43. port.msg_count += len(retval[port])
  44. return retval
  45. def atomicOutputGeneration(self, aDEVS, time):
  46. """
  47. AtomicDEVS function to generate output, invokes the outputFnc function of the model.
  48. :param aDEVS: the AtomicDEVS model that generates the output
  49. :param time: the time at which the output must be generated
  50. :returns: dict -- the generated output
  51. """
  52. aDEVS.my_output = aDEVS.outputFnc()
  53. # Being here means that this model created output, so it triggered its internal transition
  54. # save this knowledge in the basesimulator for usage in the actual transition step
  55. self.transitioning[aDEVS] |= 1
  56. return aDEVS.my_output
  57. def massAtomicTransitions(self, trans, clock):
  58. """
  59. AtomicDEVS function to perform all necessary transitions,
  60. does so on a collection of models for performance.
  61. :param trans: iterable containing all models and their requested transition
  62. :param clock: the time at which the transition must happen
  63. """
  64. t, age = clock
  65. partialmod = []
  66. for aDEVS in trans:
  67. ttype = trans[aDEVS]
  68. ###########
  69. ## Memoization and activity tracking code
  70. ## Skipped in local simulation
  71. if not self.temporary_irreversible:
  72. # Memo part
  73. if self.memoization and len(aDEVS.memo) >= 2:
  74. found = False
  75. prev = aDEVS.memo.pop()
  76. memo = aDEVS.memo[-1]
  77. if memo.time_last == clock and prev.loadState() == aDEVS.state:
  78. if ttype == 1:
  79. found = True
  80. elif aDEVS.my_input == memo.my_input:
  81. # Inputs should be equal too
  82. if ttype == 3:
  83. found = True
  84. elif aDEVS.elapsed == memo.elapsed and ttype == 2:
  85. found = True
  86. if found:
  87. aDEVS.state = memo.loadState()
  88. aDEVS.time_last = clock
  89. aDEVS.time_next = memo.time_next
  90. # Just add the copy
  91. aDEVS.old_states.append(memo)
  92. if self.do_some_tracing:
  93. # Completely skip all these calls if no tracing, saves us a lot of function calls
  94. if ttype == 1:
  95. self.tracers.tracesInternal(aDEVS)
  96. elif ttype == 2:
  97. self.tracers.tracesExternal(aDEVS)
  98. elif ttype == 3:
  99. self.tracers.tracesConfluent(aDEVS)
  100. aDEVS.my_input = {}
  101. if self.relocation_pending:
  102. # Quit ASAP by throwing an exception
  103. raise QuickStopException()
  104. continue
  105. else:
  106. aDEVS.memo = []
  107. activity_tracking_prevalue = aDEVS.preActivityCalculation()
  108. elif self.activity_tracking:
  109. activity_tracking_prevalue = aDEVS.preActivityCalculation()
  110. ###########
  111. # Make a copy of the message before it is passed to the user
  112. if self.msg_copy != 2:
  113. # Prevent a pass statement, which still consumes some time in CPython
  114. if self.msg_copy == 1:
  115. # Using list comprehension inside of dictionary comprehension...
  116. aDEVS.my_input = {key:
  117. [i.copy() for i in aDEVS.my_input[key]]
  118. for key in aDEVS.my_input}
  119. elif self.msg_copy == 0:
  120. # Dictionary comprehension
  121. aDEVS.my_input = {key:
  122. pickle.loads(pickle.dumps(aDEVS.my_input[key],
  123. pickle.HIGHEST_PROTOCOL))
  124. for key in aDEVS.my_input}
  125. # NOTE ttype mappings: (EI)
  126. # 1 -- Internal transition (01)
  127. # 2 -- External transition (10)
  128. # 3 -- Confluent transition (11)
  129. if ttype == 1:
  130. # Internal only
  131. aDEVS.elapsed = None
  132. aDEVS.state = aDEVS.intTransition()
  133. elif ttype == 2:
  134. # External only
  135. aDEVS.elapsed = t - aDEVS.time_last[0]
  136. aDEVS.state = aDEVS.extTransition(aDEVS.my_input)
  137. elif ttype == 3:
  138. # Confluent
  139. aDEVS.elapsed = 0.
  140. aDEVS.state = aDEVS.confTransition(aDEVS.my_input)
  141. else:
  142. raise DEVSException(
  143. "Problem in transitioning dictionary: unknown element %s"
  144. % ttype)
  145. ta = aDEVS.timeAdvance()
  146. aDEVS.time_last = clock
  147. if ta < 0:
  148. raise DEVSException("Negative time advance in atomic model '" + \
  149. aDEVS.getModelFullName() + "' with value " + \
  150. str(ta) + " at time " + str(t))
  151. # Update the time, this is just done in the timeNext, as this will propagate to the basesimulator
  152. aDEVS.time_next = (t + ta, 1 if ta else (age + 1))
  153. # Save the state
  154. if not self.temporary_irreversible:
  155. partialmod.append(aDEVS)
  156. # But only if there are multiple kernels, since otherwise there would be no other kernel to invoke a revertion
  157. # This can save us lots of time for local simulation (however, all other code is written with parallellisation in mind...)
  158. activity = aDEVS.postActivityCalculation(activity_tracking_prevalue)
  159. aDEVS.old_states.append(self.state_saver(aDEVS.time_last,
  160. aDEVS.time_next,
  161. aDEVS.state,
  162. activity,
  163. aDEVS.my_input,
  164. aDEVS.elapsed))
  165. if self.relocation_pending:
  166. # Quit ASAP by throwing an exception
  167. for m in partialmod:
  168. # Roll back these models to before the transitions
  169. m.time_next = m.old_states[-1].time_next
  170. m.time_last = m.old_states[-1].time_last
  171. m.state = m.old_states[-1].loadState()
  172. self.model.scheduler.massReschedule(trans)
  173. self.server.flushQueuedMessages()
  174. raise QuickStopException()
  175. elif self.activity_tracking:
  176. activity = aDEVS.postActivityCalculation(activity_tracking_prevalue)
  177. self.total_activities[aDEVS.model_id] += activity
  178. if self.do_some_tracing:
  179. # Completely skip all these calls if no tracing, saves us a lot of function calls
  180. if ttype == 1:
  181. self.tracers.tracesInternal(aDEVS)
  182. elif ttype == 2:
  183. self.tracers.tracesExternal(aDEVS)
  184. elif ttype == 3:
  185. self.tracers.tracesConfluent(aDEVS)
  186. # Clear the bag
  187. aDEVS.my_input = {}
  188. self.server.flushQueuedMessages()
  189. def atomicInit(self, aDEVS, time):
  190. """
  191. AtomicDEVS function to initialise the model
  192. :param aDEVS: the model to initialise
  193. """
  194. aDEVS.time_last = (time[0] - aDEVS.elapsed, 1)
  195. ta = aDEVS.timeAdvance()
  196. if ta < 0:
  197. raise DEVSException("Negative time advance in atomic model '" + \
  198. aDEVS.getModelFullName() + "' with value " + \
  199. str(ta) + " at initialisation")
  200. aDEVS.time_next = (aDEVS.time_last[0] + ta, 1)
  201. # Save the state
  202. if not self.irreversible:
  203. aDEVS.old_states.append(self.state_saver(aDEVS.time_last,
  204. aDEVS.time_next,
  205. aDEVS.state,
  206. 0.0,
  207. {},
  208. 0.0))
  209. # All tracing features
  210. self.tracers.tracesInit(aDEVS, time)
  211. def coupledOutputGenerationClassic(self, time):
  212. """
  213. CoupledDEVS function to generate the output, calls the atomicDEVS models where necessary. Output is routed too.
  214. :param time: the time at which output should be generated
  215. :returns: the models that should be rescheduled
  216. """
  217. cDEVS = self.model
  218. imminent = cDEVS.scheduler.getImminent(time)
  219. if not imminent:
  220. # For real time simulation, when a model is interrupted
  221. return self.transitioning
  222. reschedule = set(imminent)
  223. for model in imminent:
  224. model.time_next = (model.time_next[0], model.time_next[1] + 1)
  225. # Return value are the models to reschedule
  226. # self.transitioning are the models that must transition
  227. if len(imminent) > 1:
  228. # Perform all selects
  229. imminent.sort(key=lambda i: i.getModelFullName())
  230. pending = imminent
  231. level = 1
  232. while len(pending) > 1:
  233. # Take the model each time, as we need to make sure that the selectHierarchy is valid everywhere
  234. model = pending[0]
  235. # Make a set first to remove duplicates
  236. colliding = list(set([m.select_hierarchy[level] for m in pending]))
  237. chosen = model.select_hierarchy[level-1].select(
  238. sorted(colliding, key=lambda i:i.getModelFullName()))
  239. pending = [m for m in pending
  240. if m.select_hierarchy[level] == chosen]
  241. level += 1
  242. child = pending[0]
  243. else:
  244. child = imminent[0]
  245. # Recorrect the timeNext of the model that will transition
  246. child.time_next = (child.time_next[0], child.time_next[1] - 1)
  247. outbag = child.my_output = ClassicDEVSWrapper(child).outputFnc()
  248. self.transitioning[child] = 1
  249. for outport in outbag:
  250. for inport, z in outport.routing_outline:
  251. payload = outbag[outport]
  252. if z is not None:
  253. payload = [z(pickle.loads(pickle.dumps(m))) for m in payload]
  254. aDEVS = inport.host_DEVS
  255. aDEVS.my_input[inport] = list(payload)
  256. self.transitioning[aDEVS] = 2
  257. reschedule.add(aDEVS)
  258. # We have now generated the transitioning variable, though we need some small magic to have it work for classic DEVS
  259. self.transitioning = {ClassicDEVSWrapper(m): self.transitioning[m]
  260. for m in self.transitioning}
  261. return reschedule
  262. def coupledOutputGeneration(self, time):
  263. """
  264. CoupledDEVS function to generate the output, calls the atomicDEVS models where necessary. Output is routed too.
  265. :param time: the time at which output should be generated
  266. :returns: the models that should be rescheduled
  267. """
  268. cDEVS = self.model
  269. remotes = {}
  270. for child in cDEVS.scheduler.getImminent(time):
  271. outbag = self.atomicOutputGeneration(child, time)
  272. for outport in outbag:
  273. payload = outbag[outport]
  274. if not hasattr(outport, "routing_outline"):
  275. raise Exception(outport)
  276. for inport, z in outport.routing_outline:
  277. aDEVS = inport.host_DEVS
  278. if z is not None:
  279. payload = [z(pickle.loads(pickle.dumps(m)))
  280. for m in payload]
  281. if aDEVS.model_id in self.model.local_model_ids:
  282. # This setdefault call is responsible for our non-linear runtime in several situations...
  283. aDEVS.my_input.setdefault(inport, []).extend(payload)
  284. self.transitioning[aDEVS] |= 2
  285. else:
  286. remotes.setdefault(aDEVS.model_id,
  287. {}).setdefault(inport.port_id,
  288. []).extend(payload)
  289. for destination in remotes:
  290. self.send(destination, time, remotes[destination])
  291. return self.transitioning
  292. def coupledInit(self):
  293. """
  294. CoupledDEVS function to initialise the model, calls all its _local_ children too.
  295. """
  296. cDEVS = self.model
  297. time_next = (float('inf'), 1)
  298. # This part isn't fast, but it doesn't matter, since it just inits everything, optimizing here doesn't
  299. # matter as it is only called once AND every element has to be initted.
  300. # Only local models should receive this initialisation from us
  301. for d in self.local:
  302. self.atomicInit(d, (0.0, 0))
  303. time_next = min(time_next, d.time_next)
  304. # NOTE do not immediately assign to the timeNext, as this is used in the GVT algorithm to see whether a node has finished
  305. cDEVS.time_next = time_next
  306. self.model.setScheduler(self.model.scheduler_type)
  307. self.server.flushQueuedMessages()
  308. def performDSDEVS(self, transitioning):
  309. """
  310. Perform Dynamic Structure detection of the model
  311. :param transitioning: iteratable to be checked for a dynamic structure transiton
  312. """
  313. #TODO setting the server is very dirty
  314. self.dc_altered = set()
  315. for m in transitioning:
  316. m.server = self
  317. iterlist = [aDEVS.parent for aDEVS in transitioning
  318. if aDEVS.modelTransition(self.dsdevs_dict)]
  319. # Contains all models that are already checked, to prevent duplicate checking.
  320. # This was not necessary for atomic models, as they are guaranteed to only be called
  321. # once, as they have no children to induce a structural change on them
  322. checked = set()
  323. while iterlist:
  324. new_iterlist = []
  325. for cDEVS in iterlist:
  326. cDEVS.server = self
  327. if cDEVS is None:
  328. # Problematic
  329. #assert warning("Root DEVS returned True in the modelTransition method; ignoring")
  330. continue
  331. if cDEVS in checked:
  332. continue
  333. checked.add(cDEVS)
  334. if cDEVS.modelTransition(self.dsdevs_dict):
  335. new_iterlist.append(cDEVS.parent)
  336. # Don't update the iterlist while we are iterating over it
  337. iterlist = new_iterlist
  338. if self.dc_altered:
  339. self.model.redoDirectConnection(self.dc_altered)