فهرست منبع

Plotting module now kinda finished

Randy Paredis 3 سال پیش
والد
کامیت
89a8b22eaf

BIN
doc/_figures/arrow-wave.gif


BIN
doc/_figures/sine-wave-bokeh.gif


BIN
doc/_figures/sine-wave-scatter.gif


BIN
doc/_figures/sine-wave-step.gif


+ 4 - 1
doc/_static/style.css

@@ -2,6 +2,7 @@
  * Additional Styling
  */
 
+/*
 @media screen and (min-width: 1101px) {
 	.section:before {
 		display: block;
@@ -76,7 +77,7 @@ article.catalyst-article .class table tbody tr td.field-body p.admonition-title
 
 .MathJax {
 	font-size: 200% !important;
-}
+} */
 
 .section img:not(.next-page):not(.previous-page) {	/* Centers Images */
 	display: block;
@@ -84,6 +85,7 @@ article.catalyst-article .class table tbody tr td.field-body p.admonition-title
 	margin-right: auto;
 }
 
+/*
 div.math > p > img {
 	display: inline-block;
 	margin-left: unset;
@@ -106,3 +108,4 @@ article.catalyst-article .class dl dt em.property {
 	font-style: italic;
 	margin-right: 1em;
 }
+ */

+ 2 - 1
doc/conf.py

@@ -43,6 +43,7 @@ autodoc_member_order = 'bysource'
 # ones.
 extensions = [
     'sphinx.ext.autodoc',
+    'sphinx.ext.mathjax',
     'sphinx.ext.viewcode',
     'sphinx.ext.todo',
     'sphinx.ext.napoleon',
@@ -105,7 +106,7 @@ html_sidebars = { '**': ['globaltoc.html', 'relations.html', 'sourcelink.html',
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
 html_static_path = ['_static']
-html_css_files = []
+html_css_files = ['style.css']
 html_js_files = ['math.js']
 
 # Custom sidebar templates, must be a dictionary that maps document names

+ 68 - 12
doc/examples/LivePlot.rst

@@ -24,13 +24,6 @@ Notice you also need a block that stores the data. For plotting a single signal,
 :class:`CBD.lib.endpoints.SignalCollectorBlock`. Alternatively, to plot XY-pairs, the
 :class:`CBD.lib.endpoints.PositionCollectorBlock` can be used.
 
-Following the Signal
-^^^^^^^^^^^^^^^^^^^^
-The most power of the plotting module comes from the :meth:`CBD.realtime.plotting.follow` method.
-This function allows the plots to follow the data in real-time. Take a look at the documentation for
-a detailed explanation on how this function can be used. The following examples will use this
-method to ensure the most recent plot value is centered in the view.
-
 Example Model
 ^^^^^^^^^^^^^
 The examples below show how you can display a live plot for the :doc:`SinGen`, plotted in realtime.
@@ -195,10 +188,6 @@ this command, plots may start to "flicker" as the updates take too long.
 
     bokeh serve <experiment file>
 
-.. warning::
-    While functional for the most part, live plotting using `Bokeh` is still in beta. Not all features will work
-    as expected.
-
 .. note::
     In order to ensure that the :meth:`follow` function works for the x-axis, it is pertinent to set the
     :code:`x_range` attribute of the figure to the starting range. The same must be done for the y-axis.
@@ -227,7 +216,8 @@ this command, plots may start to "flicker" as the updates take too long.
     # Use the Bokeh Backend
     manager = PlotManager(Backend.BOKEH)
     manager.register("sin", sinGen.findBlock('collector')[0], fig, LinePlot(color='red'))
-    manager.connect('sin', 'update', lambda d: manager.bokeh_set_xlim(fig, document, follow(d[0], 10.0, lower_bound=0.0)))
+    manager.connect('sin', 'update', lambda d:
+                        manager.bokeh_set_xlim(fig, document, follow(d[0], 10.0, lower_bound=0.0)))
 
     sim = Simulator(sinGen)
     sim.connect("finished", manager.stop)  #<< Stop polling the plots for updates
@@ -238,3 +228,69 @@ this command, plots may start to "flicker" as the updates take too long.
 
 .. figure:: ../_figures/sine-wave-bokeh.gif
     :width: 400
+
+.. warning::
+    The simulation keeps running in the backend until the server is (requested to be) terminated. This is
+    because `Bokeh` does not have accurate client closure hooks. Please contact the repo authors if you find
+    a way to do this. Normally, users should not experience any issues because of this.
+
+Configuration
+^^^^^^^^^^^^^
+The :mod:`CBD.realtime.plotting` module has a lot of configuration possibilities and options that allow
+a wide range of visualisations. The examples above only differ in the plotting backend, but there exist
+many more possibilities.
+
+Following the Signal
+--------------------
+Notice how the above examples all have a line similar to:
+
+.. code-block:: python
+
+    manager.connect('sin', 'update', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
+
+This line connects a callback function that must be executed each time the :code:`sin` handler updates. In the
+case above, the callback function will update the x-axis limits by using the powerful
+:meth:`CBD.realtime.plotting.follow` method. It will follow the most recent value by using a sliding window of
+size 10. The signal will be kept in the center (default) and the window will not show values lower than 0.
+
+It's this function that allows the nice looking plot following. Take a look at the documentation for
+a detailed explanation on how this function can be used for more complex scenarios.
+
+.. note::
+    If you want to change both axes, either group the axis update in a helper function, or connect multiple
+    callback functions.
+
+Different Kinds of Plots
+------------------------
+Besides the default line plot, there are some additional kinds provided. Each of these plot kinds allow
+configuration using the backend (keyword) arguments. These are passed to the manager during registration
+(notice the :code:`LinePlot` class in the code above). Simply changing this class can produce different
+results.
+
+.. glossary::
+
+    Line Plot (:class:`CBD.realtime.plotting.LinePlot`)
+        The most common line plot was used in the above examples. It draws a straight line between all
+        sequential points in the given dataset.
+
+    Step Plot (:class:`CBD.realtime.plotting.StepPlot`)
+        A line plot that applies zero-order hold mechanics. Instead of drawing a straight line to the next
+        data point, it will stay horizontal and will "jump" up stepwise.
+
+        .. figure:: ../_figures/sine-wave-step.gif
+            :width: 400
+
+    Scatter Plot (:class:`CBD.realtime.plotting.ScatterPlot`)
+        Only draws the data points, does not create a line between them.
+
+        .. figure:: ../_figures/sine-wave-scatter.gif
+            :width: 400
+
+    Arrow (:class:`CBD.realtime.plotting.Arrow`)
+        Draws an arrow vector from a given :code:`position`, with a certain :code:`size`. This uses the
+        latest y value from the data as the (radian) angle amongst the unit circle (i.e., counter-clockwise
+        = positive angle). The :code:`update` signal may also update the :code:`position` and the :code:`size`.
+
+        .. figure:: ../_figures/arrow-wave.gif
+            :width: 400
+

+ 1 - 1
doc/install.rst

@@ -36,7 +36,7 @@ Next, there are some additional **optional** requirements:
 
 * **Documentation:**
 
-  * `Catalyst Sphynx Theme <https://pypi.org/project/catalyst-sphinx-theme/>`_ for creating the docs.
+  * `Sphinx-Theme <https://pypi.org/project/sphinx-theme/>`_ for creating the docs.
 
 Installation
 ------------

+ 7 - 5
examples/scripts/LivePlot/SinGen_bokeh_experiment.py

@@ -1,23 +1,25 @@
 from SinGen import *
-from CBD.realtime.plotting import PlotManager, Backend, LinePlot, follow
+from CBD.realtime.plotting import PlotManager, Backend, StepPlot, follow
 from CBD.simulator import Simulator
+import sys, logging
 
 from bokeh.plotting import figure, curdoc
 
 sinGen = SinGen("sin")
 
-fig = figure(plot_width=500, plot_height=500, x_range=(0, 10), y_range=(-1, 1))
+fig = figure(plot_width=500, plot_height=500, x_range=(0, 0), y_range=(-1, 1))
 document = curdoc()
 document.add_root(fig)
 
 # Use the Bokeh Backend
 manager = PlotManager(Backend.BOKEH)
-manager.register("sin", sinGen.findBlock('collector')[0], fig, LinePlot(color='red'))
+manager.register("sin", sinGen.findBlock('collector')[0], fig, StepPlot(color='green', line_width=3))
 
-manager.connect('sin', 'update', lambda d: manager.bokeh_set_xlim(fig, document, follow(d[0], 10.0, lower_bound=0.0)))
+manager.connect('sin', 'update', lambda d:
+					manager.bokeh_set_xlim(fig, document, follow(d[0], 10.0, lower_bound=0.0)))
 
 sim = Simulator(sinGen)
 sim.connect("finished", manager.stop)
 sim.setRealTime()
 sim.setDeltaT(0.1)
-sim.run(20.0)
+sim.run(200.0)

+ 2 - 1
requirements.txt

@@ -1,3 +1,4 @@
 matplotlib
 bokeh
-tqdm  # for progress bar
+tqdm          # for progress bar
+sphinx_theme  # for docs

+ 25 - 40
src/CBD/realtime/plotting.py

@@ -34,7 +34,6 @@ except ImportError:
 # __all__ = ['Backend', 'PlotKind', 'PlotHandler', 'PlotManager', 'plot', 'follow', 'set_xlim', 'set_ylim',
 #            'Arrow', 'StepPlot', 'ScatterPlot', 'LinePlot']
 
-# TODO: Bokeh (see TODOs)
 # TODO: More Plot Kinds
 
 class Backend:
@@ -190,8 +189,7 @@ class PlotHandler:
 				figure[0].canvas.mpl_connect('close_event', lambda _: self.__close_event())
 			elif Backend.compare("BOKEH", backend):
 				self.__periodic_callback = bokeh.io.curdoc().add_periodic_callback(lambda: self.update(), interval)
-				# TODO (is this even possible?):
-				bokeh.io.curdoc().on_session_destroyed(lambda ctx: self.__close_event())
+				# No need to do closing callback -- automatically done by simulator
 
 	def signal(self, name, *args):
 		"""
@@ -292,9 +290,6 @@ class PlotHandler:
 		"""
 		if self.kind.is_backend("MPL", "SNS"):
 			plt.close(self.figure[0])
-		elif self.kind.is_backend("BOKEH"):
-			# TODO
-			pass
 		# Make sure we close the plot if the backend fails to do so
 		self.__close_event()
 
@@ -306,7 +301,6 @@ class PlotHandler:
 			This function is only used in the backend to prevent double
 			closing of the plots.
 		"""
-		# print("closed")
 		if self.__opened:
 			self.__opened = False
 			self.stop()
@@ -565,11 +559,6 @@ class LinePlot(PlotKind):
 		- **matplotlib:** :func:`matplotlib.axes.Axes.plot`
 		- **bokeh:** :func:`bokeh.plotting.Figure.line`
 		- **seaborn:** :func:`seaborn.lineplot`
-
-	See Also:
-		- :class:`PlotKind`
-		- :class:`StepPlot`
-		- :class:`ScatterPlot`
 	"""
 	def create(self, figure):
 		if self.is_backend("MPL"):
@@ -577,9 +566,6 @@ class LinePlot(PlotKind):
 			line, = figure[1].plot([], [], *self.args, **self.kwargs)
 			return line
 		elif self.is_backend("BOKEH"):
-			# source = bokeh.models.ColumnDataSource({'x': [], 'y': []})
-			# figure.line(source=source, *self.args, **self.kwargs)
-			# return source
 			return figure.line([], [], *self.args, **self.kwargs)
 		elif self.is_backend("SNS"):
 			# matplotlib: figure[1] is the axis
@@ -602,11 +588,6 @@ class StepPlot(PlotKind):
 		- **matplotlib:** :func:`matplotlib.axes.Axes.step`
 		- **bokeh:** :func:`bokeh.plotting.Figure.step`
 		- **seaborn:** :func:`seaborn.lineplot` with :code:`drawstyle='steps-pre'`
-
-	See Also:
-		- :class:`PlotKind`
-		- :class:`LinePlot`
-		- :class:`StepPlot`
 	"""
 	def create(self, figure):
 		if self.is_backend("MPL"):
@@ -636,11 +617,6 @@ class ScatterPlot(PlotKind):
 		- **matplotlib:** :func:`matplotlib.axes.Axes.scatter`
 		- **bokeh:** :func:`bokeh.plotting.Figure.scatter`
 		- **seaborn:** :func:`seaborn.scatterplot`
-
-	See Also:
-		- :class:`PlotKind`
-		- :class:`LinePlot`
-		- :class:`StepPlot`
 	"""
 	def create(self, figure):
 		if self.is_backend("MPL"):
@@ -661,47 +637,56 @@ class ScatterPlot(PlotKind):
 		elif self.is_backend("BOKEH"):
 			element.data_source.data.update(x=data[0], y=data[1])
 
+import math
 
 class Arrow(PlotKind):
 	"""
-	Draws a vector at the given position
+	Draws a "direction" arrow at a position, indicative of an angle.
+	This :class:`PlotKind` assumes that the y-component indicates the
+	angle.
 
 	Backend Information:
 		- **matplotlib:** :func:`matplotlib.axes.Axes.scatter`
 		- **bokeh:** Not available.
 		- **seaborn:** Not available.
 	"""
-	def __init__(self, size, *args, **kwargs):
+	def __init__(self, size, position, *args, **kwargs):
 		PlotKind.__init__(self, *args, **kwargs)
-		self.position = 0.0, 0.0
+		self.position = position
 		self.size = size
 
 	def create(self, figure):
-		if self.is_backend("MPL"):
+		x, y = self.position
+		if self.is_backend("MPL", "SNS"):
 			# matplotlib: figure[1] is the axis
-			x, y = self.position
-			arrow = mplArrow(x, y, self.size, self.size, *self.args, **self.kwargs)
+			arrow = mplArrow(x, y, 0, 0, *self.args, **self.kwargs)
 			line = figure[1].add_patch(arrow)
 			line.set_zorder(20)
 			return line
-		elif self.is_backend("BOKEH", "SNS"):
-			raise NotImplementedError("This feature is not (yet) available for this backend.")
+		elif self.is_backend("BKH"):
+			arrow = bokeh.models.Arrow(x_start=x, y_start=y, x_end=x, y_end=y, **self.kwargs)
+			figure.add_layout(arrow)
+			return arrow
 
 	def update(self, element, *data):
 		heading = data[1]
-		x = math.cos(heading[-1]) * self.size
-		y = math.sin(heading[-1]) * self.size
+		dx = math.cos(heading[-1]) * self.size
+		dy = math.sin(heading[-1]) * self.size
+		x, y = self.position
 
-		if self.is_backend("MPL"):
+		if self.is_backend("MPL", "SNS"):
 			ax = element.axes
 			element.remove()
-			nx, ny = self.position
-			arrow = mplArrow(nx, ny, x, y, *self.args, **self.kwargs)
+			arrow = mplArrow(x, y, dx, dy, *self.args, **self.kwargs)
 			line = ax.add_patch(arrow)
 			line.set_zorder(20)
 			return line
-		elif self.is_backend("BOKEH", "SNS"):
-			raise NotImplementedError("This feature is not (yet) available for this backend.")
+		elif self.is_backend("BKH"):
+			element.x_start = x
+			element.y_start = y
+			element.x_end = x + dx
+			element.y_end = y + dy
+			return element
 
 
 class PlotManager:

+ 9 - 0
src/CBD/realtime/threadingBackend.py

@@ -110,6 +110,15 @@ class ThreadingBackend:
         else:
             raise Exception("Realtime subsystem not found: " + str(subsystem))
 
+    def is_alive(self):
+        """
+        Checks that the main thread is alive.
+
+        Returns:
+            :code`True` when it is alive, otherwise :code:`False`.
+        """
+        return threading.main_thread().is_alive()
+
     def wait(self, time, func):
         """
         A non-blocking call, which will call the :code:`func` parameter after

+ 4 - 1
src/CBD/simulator.py

@@ -1,5 +1,6 @@
 import sys
 import time
+import logging
 import threading
 from . import naivelog
 from .depGraph import createDepGraph
@@ -540,7 +541,9 @@ class Simulator:
 		"""
 		self.__realtime_counter = self.__realtime_counter_max
 		while True:
-			if self.__check():
+			# Force terminate when the main thread is not active anymore
+			#   There is no need to keep the simulation alive in this case
+			if not self.__threading_backend.is_alive() or self.__check():
 				self.__finish()
 				break