Procházet zdrojové kódy

Tested other backends w.r.t. plotting manager

rparedis před 5 roky
rodič
revize
09e4bbb12a

+ 1 - 1
examples/SinGen/SinGen.py

@@ -10,7 +10,7 @@ DELTA_T = 0.1
 
 class SinGen(CBD):
     def __init__(self, block_name):
-        super().__init__(block_name, input_ports=[], output_ports=[])
+        CBD.__init__(self, block_name, input_ports=[], output_ports=[])
 
         # Create the Blocks
         self.addBlock(GenericBlock("sin", block_operator=("sin")))

+ 33 - 20
examples/SinGen/SinGen_experiment.py

@@ -2,56 +2,69 @@
 # This file was automatically generated from drawio2cbd with the command:
 #   /home/red/git/DrawioConvert/__main__.py SinGen.xml -fav -F CBD -e SinGen -E delta=0.1
 
-from CBD.realtime.plotting import PlotHandler, PlotManager, LinePlot
+from CBD.realtime.plotting import PlotManager, LinePlot, follow
 from CBD.simulator import Simulator
 from SinGen import *
+import time
 import matplotlib.pyplot as plt
 
+import tkinter as tk
+from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
+
 DELTA_T = 0.1
 
-# fig = plt.figure(figsize=(15, 5), dpi=100)
-# ax = fig.add_subplot(111)
-# ax.set_ylim((-1, 1))
-# ax = None
+fig = plt.figure(figsize=(15, 5), dpi=100)
+ax = fig.add_subplot(111)
+ax.set_ylim((-1, 1))
 
 cbd = SinGen("SinGen")
 
-# manager = PlotManager()
-# manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
-# manager.connect('sin', 'update_event', lambda d, axis=ax: axis.set_xlim(PlotHandler.follow(d[0], 10.0, 0.0)))
+root = tk.Tk()
 
-# plt.show(block=False)
+canvas = FigureCanvasTkAgg(fig, master=root)  # A tk.DrawingArea.
+canvas.draw()
+canvas.get_tk_widget().grid(column=1, row=1)
 
-# plotter = cbd.getBlockByName("plot")
+manager = PlotManager()
+manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
+manager.connect('sin', 'update_event', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, upper_bound=0.0)))
+
+# plt.show(block=False)
 
 # def term(*_):
 # 	plt.draw()
 # 	plt.pause(0.01)
 # 	return not manager.is_opened()
 
-
 # Run the Simulation
-time = 10.0
+sim_time = 100.0
 sim = Simulator(cbd)
 sim.setRealTime()
 sim.setProgressBar()
 sim.setDeltaT(DELTA_T)
+sim.setRealTimePlatformTk(root)
 # sim.setTerminationCondition(term)
-# plotter.set_animation(fig)
-# plotter.start_animation(fig)
-sim.run(time)
-# plotter.end_animation()
+sim.run(sim_time)
+root.mainloop()
 
-while sim.is_running(): pass
+# while sim.is_running():
+# # 	plt.draw()
+# # 	plt.pause(0.01)
+#
+# 	# Game Loop (time managing must be done by the user):
+# 	before = time.time()
+# 	sim.realtime_gameloop_call()
+# 	time.sleep(DELTA_T - (before - time.time()))
 
-log = sim.getDurationLog()
 
+# PLOT THE DURATION LOG
+log = sim.getDurationLog()
 fig2 = plt.figure()
 ax2 = fig2.subplots()
-ax2.set_title("Block Computation (T = {:.2f}, dt = {:.2f})".format(time, DELTA_T))
+ax2.set_title("Block Computation [Plotting Manager + TkInter] (T = {:.2f}, dt = {:.2f})".format(sim_time, DELTA_T))
 ax2.set_xlabel("Iterations")
 ax2.set_ylabel("Time")
 # ax2.plot([-1, len(log)], [DELTA_T, DELTA_T], c='red')
 # ax2.plot([-1, len(log)], [0.01, 0.01], c='green')
 ax2.bar(range(len(log)), log)
-plt.show()
+# plt.show()

binární
examples/SinGen/durations-gl-10.png


binární
examples/SinGen/durations-gl-100.png


binární
examples/SinGen/durations-gl-pm-10.png


binární
examples/SinGen/durations-gl-pm-100.png


binární
examples/SinGen/durations-thread-10.png


binární
examples/SinGen/durations-thread-100.png


binární
examples/SinGen/durations-thread-pm-10.png


binární
examples/SinGen/durations-thread-pm-100.png


binární
examples/SinGen/durations-tk-10.png


binární
examples/SinGen/durations-tk-100.png


binární
examples/SinGen/durations-tk-pm-10.png


binární
examples/SinGen/durations-tk-pm-100.png


+ 1 - 4
src/CBD/CBD.py

@@ -1,13 +1,10 @@
+from .util import enum
 from collections import namedtuple
 
 InputLink = namedtuple("InputLink", ["block", "output_port"])
 Signal = namedtuple("Signal", ["time", "value"])
 
 
-def enum(**enums):
-    return type('Enum', (), enums)
-
-
 level = enum(WARNING=1, ERROR=2, FATAL=3)
 epsilon = 0.001
 

+ 0 - 0
src/CBD/lib/interface/__init__.py


+ 0 - 0
src/CBD/lib/micropython/__init__.py


+ 0 - 0
src/CBD/realtime/__init__.py


+ 94 - 23
src/CBD/realtime/plotting.py

@@ -1,21 +1,41 @@
-from enum import Enum
-
+from ..util import enum
 import matplotlib.animation as animation
 from bokeh.plotting import curdoc
 # TODO: Bokeh (see TODOs), GGplot, Seaborn
 # TODO: Jupyter
+# TODO: check if framework is installed?
 
-
-class Backend(Enum):
-	MPL        = 1
-	MATPLOTLIB = 1
-	BOKEH      = 2
-	GGPLOT     = 3
+Backend = enum(
+	MPL         = 1,
+	MATPLOTLIB  = 1,
+	BOKEH       = 2,
+	# GGPLOT      = 3
+)
 
 # Note: for Bokeh, a server must be started via 'bokeh serve'
 # Note: Seaborn is built on top of matplotlib
 
 class PlotHandler:
+	"""
+	Handles Real-Time plotting, independent of a plotting framework.
+
+	Every :code:`interval` time, data will be polled from the :code:`object`,
+	w.r.t. the given plotting :code:`backend`. The handler will use the knowledge
+	of a backend to determine how the given :code:`figure` must be updated.
+
+	This class can be used in any simulation context, where plotting data can be
+	polled from an object or a resource.
+
+	Note:
+		While technically framework-independent, matplotlib has the least external
+		overhead in your code.
+
+	Args:
+		object (Any):   The object from which data needs to be polled. By default,
+						the :code:`data_xy` attribute will be used on the object,
+						which should result in a 2xN array in the form of
+						([x1, x2, x3...], [y1, y2, y3...])
+	"""
 	def __init__(self, object, figure, kind, interval=100, backend=Backend.MPL):
 		self.object = object
 		self.kind = kind
@@ -70,21 +90,72 @@ class PlotHandler:
 		self.kind.update(self.elm, *data)
 		self.signal('update_event', data)
 
-	# TODO: follow_fill => does not center, but traces highest value(s)
-
-	@staticmethod
-	def follow(data, size, lower_bound=-float('inf'), upper_bound=float('inf')):
-		assert upper_bound - lower_bound >= size, "Invalid size: outside bounds."
-		if upper_bound - lower_bound == size:
-			return lower_bound, upper_bound
-		if len(data) == 0:
-			return -size / 2.0, size / 2.0
-		value = data[-1]
-		low = max(value - size / 2.0, lower_bound)
-		high = min(low + size, upper_bound)
-		if high == upper_bound:
-			low = high - size
-		return low, high
+def follow(data, size, lower_bound=-float('inf'), upper_bound=float('inf'), perc_keep=0.5):
+	"""
+	Compute the new limits for the given dataset if the last data point must be followed.
+	This is a convenience function for updating the plotting axes' limits. Whenever not
+	enough data is available, the axis won't scale down.
+
+	Args:
+		data (list):            The data that must be plotted on the axes. Can be a
+								shortened version of only the final n values (n > 0).
+		size (float):           The total size of the axis to show, even if the data
+								did not get there.
+		lower_bound (float):    The lower bound of the axis, i.e. the minimal value to
+								show, even if the data lies outside of this interval.
+								Defaults to :code:`-float('inf')` (-infinity = no limit).
+		upper_bound (float):    The upper bound of the axis, i.e. the maximal value to
+								show, even if the data lies outside of this interval.
+								Defaults to :code:`float('inf')` (infinity = no limit).
+		perc_keep (float):      The percentage at which the final data point must be
+								shown. When :code:`0.0`, this point is the lowest value
+								shown. When :code:`1.0`, this point is the highest value
+								shown. Use this value to change how "centered" the last
+								datapoint will be. When the axis grows in a positive
+								direction, a value of :code:`0.0` will never show the
+								data. When growing negatively, the opposite is true:
+								the data will not be shown for a value of :code:`1.0`.
+								Defaults to :code:`0.5` (= the middle).
+
+	Danger:
+		The lower bound must be strictly smaller than the upper bound.
+
+	Danger:
+		The size cannot be larger than the distance between the bounds.
+
+	Example:
+		Follow a sine wave in the positive-x direction, always keeping a width of 10, in matplotlib.::
+
+			manager = PlotManager()
+			manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
+			manager.connect('sin', 'update_event', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
+
+	Example:
+		Follow a sine wave in the negative-x direction, always keeping a width of 10, in matplotlib.::
+
+			manager = PlotManager()
+			manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
+			manager.connect('sin', 'update_event', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, upper_bound=0.0)))
+
+	Example:
+		Follow a positive sine-wave, but keep a 10% margin from the right edge of the plot, in matplotlib.::
+
+			manager = PlotManager()
+			manager.register("sin", cbd.findBlock("plot")[0], (fig, ax), LinePlot())
+			manager.connect('sin', 'update_event', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, 0.0, perc_keep=0.9)))
+	"""
+	assert lower_bound < upper_bound, "Lower bound must be strictly smaller than the upper bound."
+	assert upper_bound - lower_bound >= size, "Invalid size: outside bounds."
+	if upper_bound - lower_bound == size:
+		return lower_bound, upper_bound
+	if len(data) == 0:
+		return -size / 2.0, size / 2.0
+	value = data[-1]
+	low = max(value - size * perc_keep, lower_bound)
+	high = min(low + size, upper_bound)
+	if high == upper_bound:
+		low = high - size
+	return low, high
 
 
 class PlotKind:

+ 15 - 5
src/CBD/realtime/threadingBackend.py

@@ -13,8 +13,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from ..util import enum
 import threading
 
+Platform = enum(
+    THREADING = "python",
+    PYTHON    = "python",
+    TKINTER   = "tkinter",
+    TK        = "tkinter",
+    GAMELOOP  = "loop",
+    LOOP      = "loop"
+)
+
 class ThreadingBackend(object):
     """
     Wrapper around the actual threading backend. It will also handle interrupts and the passing of them to the calling thread.
@@ -28,15 +38,15 @@ class ThreadingBackend(object):
         """
         self.interrupted_value = None
         self.value_lock = threading.Lock()
-        if subsystem == "python":
+        if subsystem == Platform.THREADING:
             from pypdevs.realtime.threadingPython import ThreadingPython
-            self.subsystem = ThreadingPython(*args)
-        elif subsystem == "tkinter":
+            self.subsystem = ThreadingPython()
+        elif subsystem == Platform.TKINTER:
             from pypdevs.realtime.threadingTkInter import ThreadingTkInter
             self.subsystem = ThreadingTkInter(*args)
-        elif subsystem == "loop":
+        elif subsystem == Platform.GAMELOOP:
             from pypdevs.realtime.threadingGameLoop import ThreadingGameLoop
-            self.subsystem = ThreadingGameLoop(*args)
+            self.subsystem = ThreadingGameLoop()
         else:
             raise Exception("Realtime subsystem not found: " + str(subsystem))
 

+ 190 - 12
src/CBD/simulator.py

@@ -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

+ 2 - 2
src/CBD/solver.py

@@ -1,8 +1,8 @@
-import sys
 import math
 from .CBD import CBD
+from .util import PYTHON_VERSION
 
-if sys.version_info[0] == 3:
+if PYTHON_VERSION == 3:
 	# Python 2 complient
 	from functools import reduce
 

+ 6 - 0
src/CBD/util.py

@@ -0,0 +1,6 @@
+import sys
+
+PYTHON_VERSION = sys.version_info[0]
+
+def enum(**enums):
+	return type('Enum', (), enums)