simulator.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. import sys
  2. import time
  3. import threading
  4. from . import naivelog
  5. from .depGraph import createDepGraph
  6. from .solver import GaussianJordanLinearSolver
  7. from .realtime.threadingBackend import ThreadingBackend, Platform
  8. from .util import PYTHON_VERSION
  9. _TQDM_FOUND = True
  10. try:
  11. from tqdm import tqdm
  12. except ImportError:
  13. _TQDM_FOUND = False
  14. class Clock:
  15. """
  16. The clock of the simulation.
  17. Args:
  18. delta_t (float): Delay in-between timesteps in the simulation.
  19. time (float): The time to start the clock at.
  20. """
  21. def __init__(self, delta_t, time=0.0):
  22. self.__delta_t = delta_t
  23. self.__time = time
  24. self.__start_time = time
  25. def getTime(self):
  26. """
  27. Gets the current simulation time.
  28. """
  29. return self.__time
  30. def setTime(self, time):
  31. self.__time = time
  32. def getStartTime(self):
  33. """
  34. Gets the starting simulation time.
  35. """
  36. return self.__start_time
  37. def step(self):
  38. """
  39. Executes a timestep on the simulation.
  40. """
  41. self.__time = self.__time + self.__delta_t
  42. def _rewind(self):
  43. """
  44. Rewinds the simulation clock to the previous iteration, assuming the delta
  45. has not been changed.
  46. Danger:
  47. Normally, this function should only be used by the internal mechanisms
  48. of the CBD simulator, **not** by a user. Using this function without a
  49. full understanding of the simulator may result in undefined behaviour.
  50. """
  51. self.__time = self.__time - self.__delta_t
  52. def setDeltaT(self, new_delta_t):
  53. """
  54. Sets the delta in-between timesteps.
  55. Args:
  56. new_delta_t (float): The new delta.
  57. """
  58. self.__delta_t = new_delta_t
  59. def getDeltaT(self):
  60. """
  61. Obtains the current delta.
  62. """
  63. return self.__delta_t
  64. class Simulator:
  65. """
  66. Simulator for a CBD model. Allows for execution of the simulation.
  67. This class implements the semantics of CBDs.
  68. Args:
  69. model (CBD): A :class:`CBD` model to simulate.
  70. """
  71. def __init__(self, model):
  72. self.model = model
  73. self.__deltaT = 1.0
  74. self.__realtime = False
  75. self.__finished = True
  76. self.__stop_requested = False
  77. # scale of time in the simulation.
  78. self.__realtime_scale = 1.0
  79. # maximal amount of events with delay 0
  80. self.__realtime_counter_max = 100
  81. # current amount of events
  82. self.__realtime_counter = self.__realtime_counter_max
  83. # Starting time of the simulation
  84. self.__realtime_start_time = 0.0
  85. self.__termination_time = float('inf')
  86. self.__termination_condition = None
  87. # simulation data [dep graph, strong components, curIt]
  88. self.__sim_data = [None, None, 0]
  89. self.__stepsize_backend = Fixed(self.__deltaT)
  90. self.__deltas = []
  91. self.__threading_backend = None
  92. self.__threading_backend_subsystem = Platform.PYTHON
  93. self.__threading_backend_args = []
  94. self.__progress = False
  95. self.__progress_event = None
  96. self.__progress_finished = True
  97. self.__logger = naivelog.getLogger("CBD")
  98. self.__duration_log = []
  99. self.__lasttime = None
  100. self.__events = {
  101. "started": [],
  102. "finished": []
  103. }
  104. # TODO: make this variable, given more solver implementations
  105. self.__solver = GaussianJordanLinearSolver(self.__logger)
  106. def setBackend(self, back):
  107. self.__stepsize_backend = back
  108. def run(self, term_time=None, start_time=0.0):
  109. """
  110. Simulates the model.
  111. Args:
  112. term_time (float): When not :code:`None`, overwrites the
  113. termination time with the new value.
  114. start_time (float): The time at which to start the simulation.
  115. I.e. at the beginning, this amount of
  116. time has passed. Defaults to 0.
  117. """
  118. self.__finished = False
  119. self.__stop_requested = False
  120. self.model.setClock(Clock(self.getDeltaT(), start_time))
  121. if term_time is not None:
  122. self.__termination_time = term_time
  123. self.__sim_data = [None, None, 0]
  124. self.__duration_log = [] # for execution statistics
  125. self.__progress_finished = False
  126. if self.__threading_backend is None:
  127. # If there is still a backend, it is the same, so keep it!
  128. self.__threading_backend = ThreadingBackend(self.__threading_backend_subsystem,
  129. self.__threading_backend_args)
  130. if _TQDM_FOUND and self.__progress and self.__termination_time < float('inf'):
  131. # Setup progress bar if possible
  132. thread = threading.Thread(target=self.__progress_update)
  133. thread.daemon = True
  134. thread.start()
  135. if self.__realtime:
  136. self.__realtime_start_time = time.time() - start_time
  137. self.__lasttime = start_time
  138. self.signal("started")
  139. if self.__realtime:
  140. self.__threading_backend.wait(0.0, self.__runsim)
  141. else:
  142. self.__runsim()
  143. def __finish(self):
  144. """
  145. Terminate the simulation.
  146. """
  147. self.__finished = True
  148. if not self.__progress:
  149. # Whenever the progress bar is initialized, wait until it ends
  150. self.__progress_finished = True
  151. self.signal("finished")
  152. def __check(self):
  153. """
  154. Checks if the simulation still needs to continue.
  155. This is done based on the termination time and condition.
  156. Returns:
  157. :code:`True` if the simulation needs to be terminated and
  158. :code:`False` otherwise.
  159. """
  160. ret = self.__stop_requested
  161. if self.__termination_condition is not None:
  162. ret = self.__termination_condition(self.model, self.__sim_data[2])
  163. return ret or self.__termination_time <= self.getTime()
  164. def stop(self):
  165. """
  166. Requests a termination of the current running simulation.
  167. """
  168. self.__stop_requested = True
  169. def is_running(self):
  170. """
  171. Returns :code:`True` as long as the simulation is running.
  172. This is a convenience function to keep real-time simulations
  173. alive, or to interact from external sources.
  174. """
  175. return not self.__progress_finished and not self.__finished
  176. def getClock(self):
  177. """
  178. Gets the simulation clock.
  179. See Also:
  180. - :func:`getTime`
  181. - :func:`getRelativeTime`
  182. - :func:`getDeltaT`
  183. - :func:`setDeltaT`
  184. - :class:`Clock`
  185. """
  186. return self.model.getClock()
  187. def getTime(self):
  188. """
  189. Gets the current simulation time.
  190. See Also:
  191. - :func:`getClock`
  192. - :func:`getRelativeTime`
  193. - :func:`getDeltaT`
  194. - :func:`setDeltaT`
  195. - :class:`Clock`
  196. """
  197. return self.getClock().getTime()
  198. def getRelativeTime(self):
  199. """
  200. Gets the current simulation time, ignoring a starting offset.
  201. See Also:
  202. - :func:`getClock`
  203. - :func:`getTime`
  204. - :func:`getDeltaT`
  205. - :func:`setDeltaT`
  206. - :class:`Clock`
  207. """
  208. return self.getClock().getTime() - self.getClock().getStartTime()
  209. def setDeltaT(self, delta_t):
  210. """
  211. Sets the delta in-between iteration steps.
  212. Args:
  213. delta_t (float): The delta.
  214. See Also:
  215. - :func:`getClock`
  216. - :func:`getTime`
  217. - :func:`getRelativeTime`
  218. - :func:`getDeltaT`
  219. - :class:`Clock`
  220. """
  221. self.__deltaT = delta_t
  222. clock = self.getClock()
  223. if clock is not None:
  224. clock.setDeltaT(delta_t)
  225. self.__stepsize_backend.delta_t = delta_t
  226. def getDeltaT(self):
  227. """
  228. Gets the delta in-between iteration steps.
  229. See Also:
  230. - :func:`getClock`
  231. - :func:`getTime`
  232. - :func:`getRelativeTime`
  233. - :func:`setDeltaT`
  234. - :class:`Clock`
  235. """
  236. return self.__deltaT
  237. def setSimData(self, data):
  238. self.__sim_data = data
  239. def setRealTime(self, enabled=True, scale=1.0):
  240. """
  241. Makes the simulation run in (scaled) real time.
  242. Args:
  243. enabled (bool): When :code:`True`, realtime simulation will be enabled.
  244. Otherwise, it will be disabled. Defaults to :code:`True`.
  245. scale (float): Optional scaling for the simulation time. When greater
  246. than 1, the simulation will run slower than the actual
  247. time. When < 1, it will run faster.
  248. E.g. :code:`scale = 2.0` will run twice as long.
  249. Defaults to :code:`1.0`.
  250. """
  251. self.__realtime = enabled
  252. # Scale of 2 => twice as long
  253. self.__realtime_scale = scale
  254. def setProgressBar(self, enabled=True):
  255. """
  256. Use the `tqdm <https://tqdm.github.io/>`_ package to display a progress bar
  257. of the simulation.
  258. Args:
  259. enabled (bool): Whether or not to enable/disable the progress bar.
  260. Defaults to :code:`True` (= show progress bar).
  261. Raises:
  262. AssertionError: if the :code:`tqdm` module cannot be located.
  263. """
  264. assert _TQDM_FOUND, "Module tqdm not found. Progressbar is not possible."
  265. self.__progress = enabled
  266. def setTerminationCondition(self, func):
  267. """
  268. Sets the system's termination condition.
  269. Args:
  270. func: A function that takes the model and the current iteration as input
  271. and produces :code:`True` if the simulation needs to terminate.
  272. Note:
  273. When set, the progress bars (see :func:`setProgressBar`) may not work as intended.
  274. See Also:
  275. :func:`setTerminationTime`
  276. """
  277. # TODO: allow termination condition to set progressbar update value?
  278. self.__termination_condition = func
  279. def setTerminationTime(self, term_time):
  280. """
  281. Sets the termination time of the system.
  282. Args:
  283. term_time (float): Termination time for the simulation.
  284. """
  285. self.__termination_time = term_time
  286. def setRealTimePlatform(self, subsystem, *args):
  287. """
  288. Sets the realtime platform to a platform of choice.
  289. This allows more complex/efficient simulations.
  290. Calling this function automatically sets the simulation to realtime.
  291. Args:
  292. subsystem (Platform): The platform to use.
  293. args: Optional arguments for this platform.
  294. Currently, only the TkInter platform
  295. makes use of these arguments.
  296. Note:
  297. To prevent misuse of the function, please use one of the wrapper
  298. functions when you have no idea what you're doing.
  299. See Also:
  300. - :func:`setRealTimePlatformThreading`
  301. - :func:`setRealTimePlatformTk`
  302. - :func:`setRealTimePlatformGameLoop`
  303. """
  304. self.setRealTime(True)
  305. self.__threading_backend = None
  306. self.__threading_backend_subsystem = subsystem
  307. self.__threading_backend_args = args
  308. def setRealTimePlatformThreading(self):
  309. """
  310. Wrapper around the :func:`setRealTimePlatform` call to automatically
  311. set the Python Threading backend.
  312. Calling this function automatically sets the simulation to realtime.
  313. See Also:
  314. - :func:`setRealTimePlatform`
  315. - :func:`setRealTimePlatformTk`
  316. - :func:`setRealTimePlatformGameLoop`
  317. """
  318. self.setRealTimePlatform(Platform.THREADING)
  319. def setRealTimePlatformGameLoop(self):
  320. """
  321. Wrapper around the :func:`setRealTimePlatform` call to automatically
  322. set the Game Loop backend. Using this backend, it is expected the user
  323. will periodically call the :func:`realtime_gameloop_call` method to
  324. update the simulation step. Timing is still maintained internally.
  325. Calling this function automatically sets the simulation to realtime.
  326. See Also:
  327. - :func:`setRealTimePlatform`
  328. - :func:`setRealTimePlatformThreading`
  329. - :func:`setRealTimePlatformTk`
  330. - :func:`realtime_gameloop_call`
  331. - :doc:`examples/RealTime`
  332. """
  333. self.setRealTimePlatform(Platform.GAMELOOP)
  334. def setRealTimePlatformTk(self, root):
  335. """
  336. Wrapper around the :func:`setRealTimePlatform` call to automatically
  337. set the TkInter backend.
  338. Calling this function automatically sets the simulation to realtime.
  339. Args:
  340. root: TkInter root window object (tkinter.Tk)
  341. See Also:
  342. - :func:`setRealTimePlatform`
  343. - :func:`setRealTimePlatformThreading`
  344. - :func:`setRealTimePlatformGameLoop`
  345. """
  346. self.setRealTimePlatform(Platform.TKINTER, root)
  347. def realtime_gameloop_call(self, time=None):
  348. """
  349. Do a step in the realtime-gameloop platform.
  350. Args:
  351. time (float): Simulation time to be passed on. Only to be used
  352. for the alternative gameloop backend.
  353. Note:
  354. This function will only work for a :attr:`Platform.GAMELOOP` or a
  355. :attr:`Platform.GLA` simulation, after the :func:`run` method has
  356. been called.
  357. See Also:
  358. - :func:`setRealTimePlatform`
  359. - :func:`setRealTimePlatformGameLoop`
  360. - :func:`run`
  361. """
  362. self.__threading_backend.step(time)
  363. def do_single_step(self):
  364. curIt = self.__sim_data[2]
  365. # Efficiency reasons: dep graph only changes at these times
  366. # in the given set of library blocks.
  367. # TODO: Must be set to "every time" instead.
  368. if curIt < 2 or self.__sim_data[0] is None:
  369. self.__sim_data[0] = createDepGraph(self.model, curIt)
  370. self.__sim_data[1] = self.__sim_data[0].getStrongComponents(curIt)
  371. self.__computeBlocks(self.__sim_data[1], self.__sim_data[0], self.__sim_data[2])
  372. self.__sim_data[2] += 1
  373. def update_clock(self):
  374. if self.__sim_data[2] > 0:
  375. new_dt = self.__stepsize_backend.getNextStepSize(self)
  376. self.setDeltaT(new_dt)
  377. self.__deltas.append(new_dt)
  378. self.getClock().step()
  379. def step_back(self):
  380. """
  381. Rewinds the simulator to the previous iteration.
  382. Danger:
  383. Normally, this function should only be used by the internal mechanisms
  384. of the CBD simulator, **not** by a user. Using this function without a
  385. full understanding of the simulator may result in undefined behaviour.
  386. """
  387. self.getClock()._rewind()
  388. self.model._rewind()
  389. self.__sim_data[2] -= 1
  390. def getDurationLog(self):
  391. """
  392. Get the list of timings for every iteration.
  393. Warning:
  394. This function is temporary and will be removed in the future.
  395. """
  396. return self.__duration_log
  397. def getDeltaLog(self):
  398. return self.__deltas
  399. def __realtimeWait(self):
  400. """
  401. Wait until next realtime event.
  402. Returns:
  403. :code:`True` if a simulation stop is required and
  404. :code:`False` otherwise.
  405. """
  406. current_time = time.time() - self.__realtime_start_time
  407. next_sim_time = min(self.__termination_time, self.__lasttime + self.getDeltaT())
  408. # Scaled Time
  409. next_sim_time *= self.__realtime_scale
  410. # Subtract the time that we already did our computation
  411. wait_time = next_sim_time - current_time
  412. self.__lasttime = next_sim_time / self.__realtime_scale
  413. if wait_time <= 0.0:
  414. # event is overdue => force execute
  415. self.__realtime_counter -= 1
  416. if self.__realtime_counter < 0:
  417. # Too many overdue events at a time
  418. self.__realtime_counter = self.__realtime_counter_max
  419. self.__threading_backend.wait(0.01, self.__runsim)
  420. return True
  421. return False
  422. self.__realtime_counter = self.__realtime_counter_max
  423. self.__threading_backend.wait(wait_time, self.__runsim)
  424. return True
  425. def __runsim(self):
  426. """
  427. Do the actual simulation.
  428. """
  429. self.__realtime_counter = self.__realtime_counter_max
  430. while True:
  431. if self.__check():
  432. self.__finish()
  433. break
  434. self.update_clock()
  435. before = time.time()
  436. self.do_single_step()
  437. self.__duration_log.append(time.time() - before)
  438. if self.__threading_backend_subsystem == Platform.GLA:
  439. self.__threading_backend.wait(self.getDeltaT(), self.__runsim)
  440. break
  441. if self.__realtime and self.__realtimeWait():
  442. # Next event has been scheduled, kill this process
  443. break
  444. def __computeBlocks(self, sortedGraph, depGraph, curIteration):
  445. """
  446. Compute the new state of the model.
  447. Args:
  448. sortedGraph: The set of strong components.
  449. depGraph: A dependency graph.
  450. curIteration (int): Current simulation iteration.
  451. """
  452. for component in sortedGraph:
  453. if not self.__hasCycle(component, depGraph):
  454. block = component[0] # the strongly connected component has a single element
  455. block.compute(curIteration)
  456. else:
  457. # Detected a strongly connected component
  458. self.__solver.checkValidity(self.model.getPath(), component)
  459. solverInput = self.__solver.constructInput(component, curIteration)
  460. self.__solver.solve(solverInput)
  461. solutionVector = solverInput[1]
  462. for block in component:
  463. blockIndex = component.index(block)
  464. block.appendToSignal(solutionVector[blockIndex])
  465. def __hasCycle(self, component, depGraph):
  466. """
  467. Determine whether a component is cyclic or not.
  468. Args:
  469. component (list): The set of strong components.
  470. depGraph: The dependency graph.
  471. """
  472. assert len(component) >= 1, "A component should have at least one element"
  473. if len(component) > 1:
  474. return True
  475. else: # a strong component of size one may still have a cycle: a self-loop
  476. if depGraph.hasDependency(component[0], component[0]):
  477. return True
  478. else:
  479. return False
  480. def __progress_update(self):
  481. """
  482. Updates the progress bar.
  483. """
  484. assert _TQDM_FOUND, "Module tqdm not found. Progressbar is not possible."
  485. end = self.__termination_time
  486. pbar = tqdm(total=end, bar_format='{desc}: {percentage:3.0f}%|{bar}| {n:.2f}/{total_fmt} '
  487. '[{elapsed}/{remaining}, {rate_fmt}{postfix}]')
  488. last = 0.0
  489. while not self.__finished:
  490. now = self.getTime()
  491. # print(end, now, last)
  492. pbar.update(min(now, end) - last)
  493. last = now
  494. time.sleep(0.5) # Only update every half a second
  495. if last < end:
  496. pbar.update(end - last)
  497. pbar.close()
  498. # TODO: prints immediately after break pbar...
  499. self.__progress_finished = True
  500. def connect(self, name, function):
  501. """
  502. Connect an event with an additional function.
  503. The functions will be called in the order they were connected to the
  504. events, with the associated arguments. The accepted signals are:
  505. - :code:`started`: Raised whenever the simulation setup has completed,
  506. but before the actual simulation begins.
  507. - :code:`finished`: Raised whenever the simulation finishes.
  508. Args:
  509. name (str): The name of the signal to raise.
  510. function: A function that will be called with the optional arguments
  511. whenever the event is raised.
  512. """
  513. if name not in self.__events:
  514. raise ValueError("Invalid signal '%s' in Simulator." % name)
  515. self.__events[name].append(function)
  516. def signal(self, name, *args):
  517. """
  518. Raise a signal with a specific name and arguments.
  519. The accepted signals are:
  520. - :code:`started`: Raised whenever the simulation setup has completed,
  521. but before the actual simulation begins.
  522. - :code:`finished`: Raised whenever the simulation finishes.
  523. Note:
  524. Normally, users do not need to call this function.
  525. Args:
  526. name (str): The name of the signal to raise.
  527. *args: Additional arguments to pass to the connected events.
  528. See Also:
  529. :func:`connect`
  530. """
  531. if name not in self.__events:
  532. raise ValueError("Invalid signal '%s' in Simulator." % name)
  533. for evt in self.__events[name]:
  534. evt(*args)
  535. from .stepsize import *