|
|
@@ -3,7 +3,8 @@ import threading
|
|
|
from . import naivelog
|
|
|
from .depGraph import createDepGraph
|
|
|
from .solver import GaussianJordanLinearSolver
|
|
|
-from .realtime.threadingBackend import ThreadingBackend
|
|
|
+from .realtime.threadingBackend import ThreadingBackend, Platform
|
|
|
+from .util import PYTHON_VERSION
|
|
|
|
|
|
_TQDM_FOUND = True
|
|
|
try:
|
|
|
@@ -11,6 +12,7 @@ try:
|
|
|
except ImportError:
|
|
|
_TQDM_FOUND = False
|
|
|
|
|
|
+
|
|
|
class Clock:
|
|
|
"""
|
|
|
The clock of the simulation
|
|
|
@@ -32,6 +34,7 @@ class Clock:
|
|
|
def getDeltaT(self):
|
|
|
return self.__delta_t
|
|
|
|
|
|
+
|
|
|
class Simulator:
|
|
|
def __init__(self, model):
|
|
|
self.model = model
|
|
|
@@ -46,8 +49,10 @@ class Simulator:
|
|
|
self.__termination_condition = None
|
|
|
self.__sim_data = [None, None, 0]
|
|
|
self.__finished = True
|
|
|
- # TODO: make backend variable
|
|
|
- self.__threading_backend = ThreadingBackend("python", [])
|
|
|
+
|
|
|
+ self.__threading_backend = None
|
|
|
+ self.__threading_backend_subsystem = Platform.PYTHON
|
|
|
+ self.__threading_backend_args = []
|
|
|
|
|
|
self.__progress = False
|
|
|
self.__progress_event = None
|
|
|
@@ -65,20 +70,24 @@ class Simulator:
|
|
|
Simulate the model!
|
|
|
"""
|
|
|
self.__finished = False
|
|
|
- self.model.setClock(Clock(self.__deltaT))
|
|
|
+ self.model.setClock(Clock(self.getDeltaT()))
|
|
|
if term_time is not None:
|
|
|
self.__termination_time = term_time
|
|
|
self.__sim_data = [None, None, 0]
|
|
|
self.__duration_log = []
|
|
|
self.__progress_finished = False
|
|
|
+ if self.__threading_backend is None:
|
|
|
+ self.__threading_backend = ThreadingBackend(self.__threading_backend_subsystem,
|
|
|
+ self.__threading_backend_args)
|
|
|
|
|
|
if _TQDM_FOUND and self.__progress:
|
|
|
thread = threading.Thread(target=self.__progress_update)
|
|
|
+ thread.daemon = True
|
|
|
thread.start()
|
|
|
|
|
|
if self.__realtime:
|
|
|
self.__realtime_start_time = time.time()
|
|
|
- self.__lasttime = -self.__deltaT
|
|
|
+ self.__lasttime = -self.getDeltaT()
|
|
|
|
|
|
self.__runsim()
|
|
|
|
|
|
@@ -92,7 +101,7 @@ class Simulator:
|
|
|
"""
|
|
|
# TODO: allow for interrupts
|
|
|
current_time = time.time() - self.__realtime_start_time
|
|
|
- next_sim_time = min(self.__termination_time, self.__lasttime + self.__deltaT)
|
|
|
+ next_sim_time = min(self.__termination_time, self.__lasttime + self.getDeltaT())
|
|
|
|
|
|
# Scaled Time
|
|
|
next_sim_time *= self.__realtime_scale
|
|
|
@@ -153,42 +162,210 @@ class Simulator:
|
|
|
:code:`False` otherwise.
|
|
|
"""
|
|
|
if self.__termination_condition is not None:
|
|
|
- return self.__termination_condition(self.model, self.__sim_cur_it)
|
|
|
+ return self.__termination_condition(self.model, self.__sim_data[2])
|
|
|
return self.__termination_time <= self.getTime()
|
|
|
|
|
|
def is_running(self):
|
|
|
+ """
|
|
|
+ Returns :code:`True` as long as the simulation is running.
|
|
|
+ This is a convenience function to keep real-time simulations
|
|
|
+ alive, or to interact from external sources.
|
|
|
+ """
|
|
|
return not self.__progress_finished
|
|
|
|
|
|
def getClock(self):
|
|
|
+ """
|
|
|
+ Gets the simulation clock.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`getTime`
|
|
|
+ - :func:`getDeltaT`
|
|
|
+ - :func:`setDeltaT`
|
|
|
+ - :class:`Clock`
|
|
|
+ """
|
|
|
return self.model.getClock()
|
|
|
|
|
|
def getTime(self):
|
|
|
+ """
|
|
|
+ Gets the current simulation time.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`getClock`
|
|
|
+ - :func:`getDeltaT`
|
|
|
+ - :func:`setDeltaT`
|
|
|
+ - :class:`Clock`
|
|
|
+ """
|
|
|
return self.getClock().getTime()
|
|
|
|
|
|
def setDeltaT(self, delta_t):
|
|
|
+ """
|
|
|
+ Sets the delta in-between iteration steps.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ delta_t (float): The delta.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`getClock`
|
|
|
+ - :func:`getTime`
|
|
|
+ - :func:`getDeltaT`
|
|
|
+ - :class:`Clock`
|
|
|
+ """
|
|
|
self.__deltaT = delta_t
|
|
|
+ clock = self.getClock()
|
|
|
+ if clock is not None:
|
|
|
+ clock.setDeltaT(delta_t)
|
|
|
+
|
|
|
+ def getDeltaT(self):
|
|
|
+ """
|
|
|
+ Gets the delta in-between iteration steps.
|
|
|
|
|
|
- def setRealTime(self, rt=True, scale=1.0):
|
|
|
- self.__realtime = rt
|
|
|
+ See Also:
|
|
|
+ - :func:`getClock`
|
|
|
+ - :func:`getTime`
|
|
|
+ - :func:`setDeltaT`
|
|
|
+ - :class:`Clock`
|
|
|
+ """
|
|
|
+ return self.__deltaT
|
|
|
+
|
|
|
+ def setRealTime(self, enabled=True, scale=1.0):
|
|
|
+ """
|
|
|
+ Makes the simulation run in (scaled) real time.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ enabled (bool): When :code:`True`, realtime simulation will be enabled.
|
|
|
+ Otherwise, it will be disabled. Defaults to :code:`True`.
|
|
|
+ scale (float): Optional scaling for the simulation time. When greater
|
|
|
+ than 1, the simulation will run slower than the actual
|
|
|
+ time. When < 1, it will run faster.
|
|
|
+ E.g. :code:`scale = 2.0` will run twice as long.
|
|
|
+ Defaults to :code:`1.0`.
|
|
|
+ """
|
|
|
+ self.__realtime = enabled
|
|
|
# Scale of 2 => twice as long
|
|
|
self.__realtime_scale = scale
|
|
|
|
|
|
def setProgressBar(self, enabled=True):
|
|
|
+ """
|
|
|
+ Use the `tdqm <https://tqdm.github.io/>`_ package to display a progress bar
|
|
|
+ of the simulation.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ enabled (bool): Whether or not to enable/disable the progress bar.
|
|
|
+ Defaults to :code:`True` (= show progress bar).
|
|
|
+
|
|
|
+ Raises:
|
|
|
+ :code:`AssertionError` if the :code:`tqdm` module cannot be located.
|
|
|
+ """
|
|
|
assert _TQDM_FOUND, "Module tqdm not found. Progressbar is not possible."
|
|
|
self.__progress = enabled
|
|
|
|
|
|
def setTerminationCondition(self, func):
|
|
|
+ """
|
|
|
+ Sets the system's termination condition.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ func: A function that takes the model and the current iteration as input
|
|
|
+ and produces :code:`True` if the simulation needs to terminate.
|
|
|
+
|
|
|
+ Note:
|
|
|
+ When set, the progress bars (see :func:`setProgressBar`) may not work as intended.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ :func:`setTerminationTime`
|
|
|
+ """
|
|
|
+ # TODO: allow termination condition to set progressbar update value?
|
|
|
self.__termination_condition = func
|
|
|
|
|
|
def setTerminationTime(self, term_time):
|
|
|
+ """
|
|
|
+ Sets the termination time of the system.
|
|
|
+ """
|
|
|
self.__termination_time = term_time
|
|
|
|
|
|
+ def setRealTimePlatform(self, subsystem, *args):
|
|
|
+ """
|
|
|
+ Sets the realtime platform to a platform of choice.
|
|
|
+ This allows more complex/efficient simulations.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ subsystem (Platform): The platform to use.
|
|
|
+ args: Optional arguments for this platform.
|
|
|
+ Currently, only the TkInter platform
|
|
|
+ makes use of these arguments.
|
|
|
+
|
|
|
+ Note:
|
|
|
+ To prevent misuse of the function, please use one of the wrapper
|
|
|
+ functions when you have no idea what you're doing.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`setRealTimePlatformThreading`
|
|
|
+ - :func:`setRealTimePlatformTk`
|
|
|
+ - :func:`setRealTimePlatformGameLoop`
|
|
|
+ """
|
|
|
+ self.__threading_backend = None
|
|
|
+ self.__threading_backend_subsystem = subsystem
|
|
|
+ self.__threading_backend_args = args
|
|
|
+
|
|
|
+ def setRealTimePlatformThreading(self):
|
|
|
+ """
|
|
|
+ Wrapper around the :func:`setRealTimePlatform` call to automatically
|
|
|
+ set the Python Threading backend.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`setRealTimePlatform`
|
|
|
+ - :func:`setRealTimePlatformTk`
|
|
|
+ - :func:`setRealTimePlatformGameLoop`
|
|
|
+ """
|
|
|
+ self.setRealTimePlatform(Platform.THREADING)
|
|
|
+
|
|
|
+ def setRealTimePlatformGameLoop(self):
|
|
|
+ """
|
|
|
+ Wrapper around the :func:`setRealTimePlatform` call to automatically
|
|
|
+ set the Python Threading backend.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`setRealTimePlatform`
|
|
|
+ - :func:`setRealTimePlatformThreading`
|
|
|
+ - :func:`setRealTimePlatformTk`
|
|
|
+ """
|
|
|
+ self.setRealTimePlatform(Platform.GAMELOOP)
|
|
|
+
|
|
|
+ def setRealTimePlatformTk(self, root):
|
|
|
+ """
|
|
|
+ Wrapper around the :func:`setRealTimePlatform` call to automatically
|
|
|
+ set the TkInter backend.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ root: TkInter root window object (tkinter.Tk)
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`setRealTimePlatform`
|
|
|
+ - :func:`setRealTimePlatformThreading`
|
|
|
+ - :func:`setRealTimePlatformGameLoop`
|
|
|
+ """
|
|
|
+ self.setRealTimePlatform(Platform.TKINTER, root)
|
|
|
+
|
|
|
+ def realtime_gameloop_call(self):
|
|
|
+ """
|
|
|
+ Do a step in the realtime-gameloop platform.
|
|
|
+
|
|
|
+ Note:
|
|
|
+ This function will only work for a :attr:`Platform.GAMELOOP` simulation,
|
|
|
+ after the :func:`run` method has been called.
|
|
|
+
|
|
|
+ See Also:
|
|
|
+ - :func:`setRealTimePlatform`
|
|
|
+ - :func:`setRealTimePlatformGameLoop`
|
|
|
+ - :func:`run`
|
|
|
+ """
|
|
|
+ self.__threading_backend.step()
|
|
|
+
|
|
|
def getDurationLog(self):
|
|
|
return self.__duration_log
|
|
|
|
|
|
def __step(self, depGraph, sortedGraph, curIteration):
|
|
|
self.__computeBlocks(sortedGraph, depGraph, curIteration)
|
|
|
- self.getClock().setDeltaT(self.__deltaT)
|
|
|
+ self.setDeltaT(self.getDeltaT())
|
|
|
self.getClock().step()
|
|
|
|
|
|
def __computeBlocks(self, sortedGraph, depGraph, curIteration):
|
|
|
@@ -208,7 +385,7 @@ class Simulator:
|
|
|
|
|
|
def __hasCycle(self, component, depGraph):
|
|
|
"""
|
|
|
- Determine whether a component is cyclic
|
|
|
+ Determine whether a component is cyclic or not.
|
|
|
"""
|
|
|
assert len(component) >= 1, "A component should have at least one element"
|
|
|
if len(component) > 1:
|
|
|
@@ -234,5 +411,6 @@ class Simulator:
|
|
|
if last < end:
|
|
|
pbar.update(end - last)
|
|
|
pbar.close()
|
|
|
- print("", flush=True, end='') # prevent printing glitches at end of simulation
|
|
|
+ # prevent printing glitches at end of simulation
|
|
|
+ print("", flush=True, end='')
|
|
|
self.__progress_finished = True
|