controller.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  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. Controller used as a specific simulation kernel
  17. """
  18. from pypdevs.basesimulator import BaseSimulator
  19. from pypdevs.logger import *
  20. import threading
  21. import pypdevs.accurate_time as time
  22. import pypdevs.middleware as middleware
  23. from pypdevs.DEVS import CoupledDEVS, AtomicDEVS
  24. from pypdevs.util import DEVSException
  25. from pypdevs.activityVisualisation import visualizeLocations
  26. from pypdevs.realtime.threadingBackend import ThreadingBackend
  27. from pypdevs.realtime.asynchronousComboGenerator import AsynchronousComboGenerator
  28. class Controller(BaseSimulator):
  29. """
  30. The controller class, which is a special kind of normal simulation kernel. This should always run on the node labeled 0.
  31. It contains some functions that are only required to be ran on a single node, such as GVT initiation
  32. """
  33. def __init__(self, name, model, server):
  34. """
  35. Constructor
  36. :param name: name of the controller
  37. :param model: model to host at the kernel
  38. :param server: the server to make requests on
  39. """
  40. BaseSimulator.__init__(self, name, model, server)
  41. self.waiting_lock = threading.Lock()
  42. self.accumulator = {}
  43. self.no_finish_ring = threading.Lock()
  44. self.no_finish_ring.acquire()
  45. self.location_cell_view = False
  46. self.graph = None
  47. self.allocations = None
  48. self.running_irreversible = None
  49. self.initial_allocator = None
  50. self.prev_termination_time = 0.0
  51. self.accept_external_input = False
  52. def __setstate__(self, retdict):
  53. """
  54. For pickling
  55. :param retdict: dictionary containing attributes and their value
  56. """
  57. BaseSimulator.__setstate__(self, retdict)
  58. self.waiting_lock = threading.Lock()
  59. self.no_finish_ring = threading.Lock()
  60. self.no_finish_ring.acquire()
  61. def GVTdone(self):
  62. """
  63. Notify this simulation kernel that the GVT calculation is finished
  64. """
  65. self.wait_for_gvt.set()
  66. def isFinished(self, running):
  67. """
  68. Checks if all kernels have indicated that they have finished simulation.
  69. If each kernel has indicated this, a final (expensive) check happens to
  70. prevent premature termination.
  71. :param running: the number of kernels that is simulating
  72. :returns: bool -- whether or not simulation is already finished
  73. """
  74. # NOTE make sure that GVT algorithm is not running at the moment, otherwise we deadlock!
  75. # it might be possible that the GVT algorithm starts immediately after the wait(), causing deadlock again
  76. # Now we are sure that the GVT algorithm is not running when we start this
  77. # It seems that we should be finished, so just ACK this with every simulation kernel before proceeding
  78. # it might be possible that the kernel's 'notifyRun' command is still on the way, making the simulation
  79. # stop too soon.
  80. self.no_finish_ring.acquire()
  81. msgcount = self.finishRing(0, 0, True)
  82. if msgcount == -1:
  83. # One of the nodes was still busy
  84. self.no_finish_ring.release()
  85. return False
  86. else:
  87. msgcount2 = self.finishRing(0, 0, True)
  88. # If they are equal, we are done
  89. ret = msgcount == msgcount2
  90. if not ret:
  91. self.no_finish_ring.release()
  92. else:
  93. self.waiting = 0
  94. return ret
  95. def waitFinish(self, running):
  96. """
  97. Wait until the specified number of kernels have all told that simulation
  98. finished.
  99. :param running: the number of kernels that is simulating
  100. """
  101. while 1:
  102. time.sleep(1)
  103. # Make sure that no relocations are running
  104. if self.isFinished(running):
  105. # All simulation kernels have told us that they are idle at the moment
  106. break
  107. self.run_gvt = False
  108. self.event_gvt.set()
  109. self.gvt_thread.join()
  110. def startGVTThread(self, gvt_interval):
  111. """
  112. Start the GVT thread
  113. :param gvt_interval: the interval between two successive GVT runs
  114. """
  115. # We seem to be the controller
  116. # Start up the GVT algorithm then
  117. self.event_gvt = threading.Event()
  118. self.run_gvt = True
  119. self.gvt_thread = threading.Thread(target=Controller.threadGVT,
  120. args=[self, gvt_interval])
  121. self.gvt_thread.daemon = True
  122. self.gvt_thread.start()
  123. def threadGVT(self, freq):
  124. """
  125. Run the GVT algorithm, this method should be called in its own thread,
  126. because it will block
  127. :param freq: the time to sleep between two GVT calculations
  128. """
  129. # Wait for the simulation to have done something useful before we start
  130. self.event_gvt.wait(freq)
  131. # Maybe simulation already finished...
  132. while self.run_gvt:
  133. self.receiveControl([float('inf'),
  134. float('inf'),
  135. self.accumulator,
  136. {}],
  137. True)
  138. # Wait until the lock is released elsewhere
  139. print("Waiting for clear")
  140. self.wait_for_gvt.wait()
  141. self.wait_for_gvt.clear()
  142. # Limit the GVT algorithm, otherwise this will flood the ring
  143. print("Cleared")
  144. self.event_gvt.wait(freq)
  145. def getVCDVariables(self):
  146. """
  147. Generate a list of all variables that exist in the current scope
  148. :returns: list -- all VCD variables in the current scope
  149. """
  150. variables = []
  151. for d in self.total_model.component_set:
  152. variables.extend(d.getVCDVariables())
  153. return variables
  154. def simulate_sync(self):
  155. """
  156. Synchronous simulation call, identical to the normal call, with the exception that it will be a blocking call as only "simulate" is marked as oneway.
  157. """
  158. BaseSimulator.simulate_sync(self)
  159. self.no_finish_ring.acquire()
  160. def simulate(self):
  161. """
  162. Run the actual simulation on the controller. This will simply 'intercept' the call to the original simulate and perform location visualisation when necessary.
  163. """
  164. self.checkForTemporaryIrreversible()
  165. self.no_finish_ring.release()
  166. if self.location_cell_view:
  167. from pypdevs.activityVisualisation import visualizeLocations
  168. visualizeLocations(self)
  169. # Call superclass (the actual simulation)
  170. BaseSimulator.simulate(self)
  171. self.prev_termination_time = self.termination_time[0]
  172. def getEventGraph(self):
  173. """
  174. Fetch a graph containing all connections and the number of events between the nodes. This is only useful when an initial allocator is chosen.
  175. :returns: dict -- containing source and destination, it will return the amount of events passed between them
  176. """
  177. return self.runAllocator()[0]
  178. def getInitialAllocations(self):
  179. """
  180. Get a list of all initial allocations. Will call the allocator to get the result.
  181. :returns: list -- containing all nodes and the models they host
  182. """
  183. return self.runAllocator()[1]
  184. def runAllocator(self):
  185. """
  186. Actually extract the graph of exchanged messages and run the allocator with this information.
  187. Results are cached.
  188. :returns: tuple -- the event graph and the allocations
  189. """
  190. # Only run this code once
  191. if self.graph is None and self.allocations is None:
  192. # It seems this is the first time
  193. if self.initial_allocator is None:
  194. # No allocator was defined, or it has already issued its allocation code, which resulted into 'nothing'
  195. self.graph = None
  196. self.allocations = None
  197. else:
  198. from pypdevs.util import constructGraph, saveLocations
  199. self.graph = constructGraph(self.model)
  200. allocs = self.initialAllocator.allocate(self.model.component_set,
  201. self.getEventGraph(),
  202. self.kernels,
  203. self.total_activities)
  204. self.allocations = allocs
  205. self.initial_allocator = None
  206. saveLocations("locationsave.txt",
  207. self.allocations,
  208. self.model_ids)
  209. return self.graph, self.allocations
  210. def setCellLocationTracer(self, x, y, location_cell_view):
  211. """
  212. Sets the Location tracer and all its configuration parameters
  213. :param x: the horizontal size of the grid
  214. :param y: the vertical size of the grid
  215. :param location_cell_view: whether or not to enable it
  216. """
  217. self.x_size = x
  218. self.y_size = y
  219. self.location_cell_view = location_cell_view
  220. def setRelocator(self, relocator):
  221. """
  222. Sets the relocator to the one provided by the user
  223. :param relocator: the relocator to use
  224. """
  225. self.relocator = relocator
  226. # Perform run-time configuration
  227. try:
  228. self.relocator.setController(self)
  229. except AttributeError:
  230. pass
  231. def setActivityTracking(self, at):
  232. """
  233. Sets the use of activity tracking, which will simply output the activity of all models at the end of the simulation
  234. :param at: whether or not to enable activity tracking
  235. """
  236. self.activity_tracking = at
  237. def setClassicDEVS(self, classic_DEVS):
  238. """
  239. Sets the use of Classic DEVS instead of Parallel DEVS.
  240. :param classicDEVS: whether or not to use Classic DEVS
  241. """
  242. # Do this once, to prevent checks for the classic DEVS formalism
  243. if classic_DEVS:
  244. # Methods, so CamelCase
  245. self.coupledOutputGeneration = self.coupledOutputGenerationClassic
  246. def setAllocator(self, initial_allocator):
  247. """
  248. Sets the use of an initial relocator.
  249. :param initial_allocator: whether or not to use an initial allocator
  250. """
  251. self.initial_allocator = initial_allocator
  252. if initial_allocator is not None:
  253. # Methods, so CamelCase
  254. self.atomicOutputGeneration_backup = self.atomicOutputGeneration
  255. self.atomicOutputGeneration = self.atomicOutputGenerationEventTracing
  256. def setDSDEVS(self, dsdevs):
  257. """
  258. Whether or not to check for DSDEVS events
  259. :param dsdevs: dsdevs boolean
  260. """
  261. self.use_DSDEVS = dsdevs
  262. def setRealtime(self, input_references):
  263. """
  264. Sets the use of realtime simulation.
  265. :param input_references: dictionary containing the string to port mapping
  266. """
  267. self.realtime = True
  268. self.realtime_port_references = input_references
  269. def setTerminationCondition(self, termination_condition):
  270. """
  271. Sets the termination condition of this simulation kernel.
  272. As soon as the condition is valid, it willl signal all nodes that they have to stop simulation as soon as they have progressed up to this simulation time.
  273. :param termination_condition: a function that accepts two parameters: *time* and *model*. Function returns whether or not to halt simulation
  274. """
  275. self.termination_condition = termination_condition
  276. self.termination_time_check = False
  277. def setAcceptExternalInputs(self, aei):
  278. """
  279. Sets the controller to accept external inputs.
  280. When enabled, the "early-return" of the simulator when all components have an infinite
  281. time-advance is ignored.
  282. """
  283. self.accept_external_input = aei
  284. def findAndPerformRelocations(self, gvt, activities, horizon):
  285. """
  286. First requests the relocator for relocations to perform, and afterwards actually perform them.
  287. :param gvt: the current GVT
  288. :param activities: list containing all activities of all nodes
  289. :param horizon: the horizon used in this activity tracking
  290. """
  291. # Now start moving all models according to the provided relocation directives
  292. relocate = self.relocator.getRelocations(gvt, activities, horizon)
  293. #print("Filtered relocate: " + str(relocate))
  294. if relocate:
  295. self.performRelocationsInit(relocate)
  296. def performRelocationsInit(self, relocate):
  297. """
  298. Perform the relocations specified in the parameter. Split of from the 'findAndPerformRelocations', to make it possible for other parts of the code
  299. to perform relocations too.
  300. :param relocate: dictionary containing the model_id as key and the value is the node to send it to
  301. """
  302. relocate = {key: relocate[key]
  303. for key in relocate
  304. if self.model_ids[key].location != relocate[key] and
  305. self.model_ids[key].relocatable}
  306. if not relocate:
  307. return
  308. if self.running_irreversible is not None:
  309. self.getProxy(self.running_irreversible).unsetIrreversible()
  310. self.running_irreversible = None
  311. while not self.no_finish_ring.acquire(False):
  312. if not self.run_gvt:
  313. self.GVTdone()
  314. return
  315. time.sleep(0)
  316. kernels = {}
  317. self.locked_kernels = set()
  318. relocation_rules = {}
  319. for model_id in relocate:
  320. source = self.model_ids[model_id].location
  321. destination = relocate[model_id]
  322. if source == destination:
  323. continue
  324. kernels[source] = kernels.get(source, 0) + 1
  325. kernels[destination] = kernels.get(destination, 0) + 1
  326. if kernels[source] == 1:
  327. # We are the first to lock it, so actually send the lock
  328. self.getProxy(source).requestMigrationLock()
  329. if kernels[destination] == 1:
  330. # We are the first to lock it, so actually send the lock
  331. self.getProxy(destination).requestMigrationLock()
  332. relocation_rules.setdefault((source, destination), set()).add(model_id)
  333. while relocation_rules:
  334. # Busy loop until everything is done
  335. # Don't use an iterator, as we will change the list
  336. for source, destination in relocation_rules.keys():
  337. if (source in self.locked_kernels and
  338. destination in self.locked_kernels):
  339. models = relocation_rules[(source, destination)]
  340. self.getProxy(source).migrateTo(destination, models)
  341. del relocation_rules[(source, destination)]
  342. kernels[source] -= len(models)
  343. kernels[destination] -= len(models)
  344. if kernels[source] == 0:
  345. self.getProxy(source).migrationUnlock()
  346. if kernels[destination] == 0:
  347. self.getProxy(destination).migrationUnlock()
  348. # OK, now check whether we need to visualize all locations or not
  349. if self.location_cell_view:
  350. visualizeLocations(self)
  351. # Possibly some node is now hosting all models, so allow this node to become irreversible for some time.
  352. self.checkForTemporaryIrreversible()
  353. # Allow the finishring algorithm again
  354. self.no_finish_ring.release()
  355. def checkForTemporaryIrreversible(self):
  356. """
  357. Checks if one node is hosting all the models. If this is the case, this node will gain 'temporary irreversibility',
  358. allowing it to skip state saving and thus avoiding the main overhead associated with time warp.
  359. """
  360. # Check whether or not everything is located at a single node now
  361. if self.relocator.useLastStateOnly():
  362. # If this is the case, we will be unable to know which state to save the activity for
  363. # So disable it for now
  364. # This does offer a slight negative impact, though it isn't really worth fixing for the time being
  365. return
  366. if isinstance(self.destinations[0], int):
  367. current_kernel = self.destinations[0]
  368. else:
  369. current_kernel = 0
  370. for kernel in self.destinations:
  371. if isinstance(kernel, int):
  372. loc = kernel
  373. else:
  374. loc = 0
  375. if loc != current_kernel:
  376. break
  377. else:
  378. # We didn't break, so one of the nodes runs all at once
  379. self.getProxy(current_kernel).setIrreversible()
  380. self.running_irreversible = current_kernel
  381. def notifyLocked(self, remote):
  382. """
  383. Notify this kernel that the model is locked
  384. :param remote: the node that is locked
  385. """
  386. self.locked_kernels.add(remote)
  387. def dsRemovePort(self, port):
  388. """
  389. Remove a port from the simulation
  390. :param port: the port to remove
  391. """
  392. for iport in port.inline:
  393. iport.outline = [p for p in iport.outline if p != port]
  394. for oport in port.outline:
  395. oport.inline = [p for p in oport.inline if p != port]
  396. self.dc_altered.add(port)
  397. def dsDisconnectPorts(self, p1, p2):
  398. """
  399. Disconnect two ports
  400. :param p1: source port
  401. :param p2: target port
  402. """
  403. self.dc_altered.add(p1)
  404. def dsConnectPorts(self, p1, p2):
  405. """
  406. Connect two ports
  407. :param p1: source port
  408. :param p2: target port
  409. """
  410. self.dc_altered.add(p1)
  411. def dsUnscheduleModel(self, model):
  412. """
  413. Dynamic Structure change: remove an existing model
  414. :param model: the model to remove
  415. """
  416. if isinstance(model, CoupledDEVS):
  417. for m in model.component_set:
  418. self.dsUnscheduleModel(m, False)
  419. for port in model.IPorts:
  420. self.dsRemovePort(port)
  421. for port in model.OPorts:
  422. self.dsRemovePort(port)
  423. elif isinstance(model, AtomicDEVS):
  424. self.model.component_set.remove(model)
  425. self.model.models.remove(model)
  426. # The model is removed, so remove it from the scheduler
  427. self.model.scheduler.unschedule(model)
  428. self.model_ids[model.model_id] = None
  429. self.destinations[model.model_id] = None
  430. self.model.local_model_ids.remove(model.model_id)
  431. for port in model.IPorts:
  432. self.dsRemovePort(port)
  433. for port in model.OPorts:
  434. self.dsRemovePort(port)
  435. else:
  436. raise DEVSException("Unknown model to schedule: %s" % model)
  437. def dsScheduleModel(self, model):
  438. """
  439. Dynamic Structure change: create a new model
  440. :param model: the model to add
  441. """
  442. if isinstance(model, CoupledDEVS):
  443. model.full_name = model.parent.full_name + "." + model.getModelName()
  444. for m in model.component_set:
  445. self.dsScheduleModel(m)
  446. for p in model.IPorts:
  447. self.dc_altered.add(p)
  448. for p in model.OPorts:
  449. self.dc_altered.add(p)
  450. elif isinstance(model, AtomicDEVS):
  451. model.model_id = len(self.model_ids)
  452. model.full_name = model.parent.full_name + "." + model.getModelName()
  453. model.location = self.name
  454. self.model_ids.append(model)
  455. self.destinations.append(model)
  456. self.model.component_set.append(model)
  457. self.model.models.append(model)
  458. self.model.local_model_ids.add(model.model_id)
  459. self.atomicInit(model, self.current_clock)
  460. p = model.parent
  461. model.select_hierarchy = [model]
  462. while p != None:
  463. model.select_hierarchy = [p] + model.select_hierarchy
  464. p = p.parent
  465. if model.time_next[0] == self.current_clock[0]:
  466. # If scheduled for 'now', update the age manually
  467. model.time_next = (model.time_next[0], self.current_clock[1])
  468. # It is a new model, so add it to the scheduler too
  469. self.model.scheduler.schedule(model)
  470. for p in model.IPorts:
  471. self.dc_altered.add(p)
  472. for p in model.OPorts:
  473. self.dc_altered.add(p)
  474. else:
  475. raise DEVSException("Unknown model to schedule: %s" % model)
  476. def setRealTime(self, subsystem, generator_file, ports, scale, listeners, args=[]):
  477. """
  478. Set the use of realtime simulation
  479. :param subsystem: defines the subsystem to use
  480. :param generator_file: filename to use for generating external inputs
  481. :param ports: input port references
  482. :param scale: the scale factor for realtime simulation
  483. :param listeners: the ports on which we should listen for output
  484. :param args: additional arguments for the realtime backend
  485. """
  486. self.realtime = True
  487. self.threading_backend = ThreadingBackend(subsystem, args)
  488. self.rt_zerotime = time.time()
  489. async_gen = AsynchronousComboGenerator(generator_file, self.threading_backend)
  490. self.asynchronous_generator = async_gen
  491. self.realtime_starttime = time.time()
  492. self.portmap = ports
  493. self.model.listeners = listeners
  494. self.realtime_scale = scale
  495. def gameLoop(self):
  496. """
  497. Perform all computations up to the current time. Only applicable for the game loop realtime backend.
  498. """
  499. self.threading_backend.step()
  500. def realtimeInterrupt(self, string):
  501. """
  502. Create an interrupt from other Python code instead of using stdin or the file
  503. :param string: the value to inject
  504. """
  505. self.threading_backend.interrupt(string)
  506. def stateChange(self, model_id, variable, value):
  507. """
  508. Notification function for when a variable's value is altered. It will notify the node that is responsible for simulation of this model AND also notify the tracers of the event.
  509. :param model_id: the model_id of the model whose variable was changed
  510. :param variable: the name of the variable that was changed (as a string)
  511. :param value: the new value of the variable
  512. """
  513. # Call the node that hosts this model and order it to recompute timeAdvance
  514. proxy = self.getProxy(self.model_ids[model_id].location)
  515. proxy.recomputeTA(model_id, self.prev_termination_time)
  516. self.tracers.tracesUser(self.prev_termination_time,
  517. self.model_ids[model_id],
  518. variable,
  519. value)