LivePlot.rst 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. Live Plotting of Data During the Simulation
  2. ===========================================
  3. During a (realtime) simulation, often you would like to show some data that's being sent over a
  4. certain connection. This can be intermediary data (i.e. the individual components of a computation),
  5. system data (battery life, sensor information...) or output information (results, actuator inputs...).
  6. Luckily, the CBD framework provides this functionality in a clean and efficient manner.
  7. To allow for "live" plotting of data, make use of the :class:`CBD.realtime.plotting.PlotManager` class,
  8. which is a wrapper for tracking multiple realtime plots. Internally, it will keep track of multiple
  9. :class:`CBD.realtime.plotting.PlotHandler` instances to reduce code-overhead.
  10. .. code-block:: python
  11. from CBD.realtime.plotting import PlotManager, ScatterPlot
  12. manager = PlotManager()
  13. # Register a scatter plot handler with name "myHandler", which listens to
  14. # the data of the block "myBlock".
  15. manager.register("myHandler", MyBlock('myBlock'), figure, ScatterPlot())
  16. Notice you also need a block that stores the data. For plotting a single signal, it's best to use the
  17. :class:`CBD.lib.endpoints.SignalCollectorBlock`. Alternatively, to plot XY-pairs, the
  18. :class:`CBD.lib.endpoints.PositionCollectorBlock` can be used.
  19. Example Model
  20. ^^^^^^^^^^^^^
  21. The examples below show how you can display a live plot for the :doc:`SinGen`, plotted in realtime.
  22. The output of this block is removed and changed to a :code:`SignalCollectorBlock`:
  23. .. code-block:: python
  24. from CBD.Core import CBD
  25. from CBD.lib.std import TimeBlock, GenericBlock
  26. from CBD.lib.endpoints import SignalCollectorBlock
  27. class SinGen(CBD):
  28. def __init__(self, name="SinGen"):
  29. CBD.__init__(self, name, input_ports=[], output_ports=[])
  30. # Create the blocks
  31. self.addBlock(TimeBlock("time"))
  32. self.addBlock(GenericBlock("sin", block_operator="sin"))
  33. self.addBlock(SignalCollectorBlock("collector"))
  34. # Connect the blocks
  35. self.addConnection("time", "sin")
  36. self.addConnection("sin", "collector")
  37. sinGen = SinGen("SinGen")
  38. Using MatPlotLib
  39. ^^^^^^^^^^^^^^^^
  40. The most common plotting framework for Python is `MatPlotLib <https://matplotlib.org/>`_. It provides
  41. a lot of additional features and functionalities, but we will keep it simple. For more complexity, please
  42. refer to their documentation.
  43. .. note::
  44. While there are other plotting frameworks, `MatPlotLib` is by far the easiest to get live plotting
  45. to work.
  46. Default
  47. -------
  48. If we're not concerned about a window manager in our system, we can easily make use of `MatPlotLib`'s
  49. builtin plotting window.
  50. .. code-block:: python
  51. from CBD.realtime.plotting import PlotManager, LinePlot, follow, set_xlim
  52. from CBD.simulator import Simulator
  53. import matplotlib.pyplot as plt
  54. fig = plt.figure(figsize=(5, 5), dpi=100)
  55. ax = fig.add_subplot(111)
  56. ax.set_ylim((-1, 1)) # The sine wave never exceeds this range
  57. plot = fig, ax
  58. manager = PlotManager()
  59. manager.register("sin", sinGen.find('collector')[0], plot, LinePlot(color='red'))
  60. manager.connect('sin', 'update', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
  61. # NOTE: alternatively, manager.set_xlim method can be used:
  62. # manager.connect('sin', 'update', lambda d, p=plot: manager.set_xlim(p, follow(d[0], 10.0, lower_bound=0.0)))
  63. sim = Simulator(sinGen)
  64. sim.setRealTime()
  65. sim.setDeltaT(0.1)
  66. sim.run(20.0)
  67. plt.show()
  68. .. figure:: ../_figures/sine-wave-mpl.gif
  69. :width: 400
  70. Seaborn
  71. -------
  72. `Seaborn <https://seaborn.pydata.org/>`_ is a data visualization library, built on top of `MatPlotLib`.
  73. Hence, it can be easily integrated and used for plotting live data. It can simply be used by providing
  74. the :code:`PlotManager`'s constructor with a backend argument (the default argument is :code:`Backend.MPL`):
  75. .. code-block:: python
  76. from CBD.realtime.plotting import Backend
  77. manager = PlotManager(Backend.SNS) # OR: Backend.SEABORN
  78. That's it. All other code remains the same. To change the theme to a `Seaborn` theme, you can either
  79. `use a MatPlotLib theme <https://matplotlib.org/stable/gallery/style_sheets/style_sheets_reference.html>`_ theme,
  80. or place the following code before the creation of the figure (see also
  81. `Seaborn's documentation <https://seaborn.pydata.org/generated/seaborn.set_theme.html#seaborn.set_theme>`_ on
  82. this topic):
  83. .. code-block:: python
  84. import seaborn as sns
  85. sns.set_theme(style="darkgrid") # or any of darkgrid, whitegrid, dark, white, ticks
  86. .. _jupyter:
  87. Jupyter Notebook
  88. ----------------
  89. These days, `Jupyter Notebooks <https://jupyter.org/>`_ are the most common way to collect experiments.
  90. Luckily, the :class:`CBD.realtime.plotting.PlotManager` can work with them without too much overhead. In fact,
  91. all that's required is setting the magic function :code:`%matplotlib` **before** creating the plot. That's it!
  92. A small caveat is the fact that a :code:`notebook` stays alive after the simulation finishes. This
  93. means the :code:`PlotManager` keeps polling for data. To stop this, connect a signal that terminates this
  94. polling to the simulator **before** starting the simulation:
  95. .. code-block:: python
  96. # Kills all polling requests and closes the plots
  97. sim.connect("finished", manager.terminate)
  98. # Kills all polling requests, but keeps plots alive
  99. sim.connect("finished", manager.stop)
  100. Also take a look at the :code:`examples/notebook` folder for more info.
  101. TkInter
  102. -------
  103. Now, as mentioned in :doc:`RealTime`, there is also a :code:`TkInter` platform to run the realtime
  104. simulation on. This can be useful for creating graphical user interfaces (GUIs). Sometimes, such a
  105. GUI might be in need of a plot of the data. See also the :doc:`Dashboard` example for a more complex
  106. variation.
  107. .. code-block:: python
  108. from CBD.realtime.plotting import PlotManager, LinePlot, follow
  109. from CBD.simulator import Simulator
  110. import tkinter as tk
  111. import matplotlib.pyplot as plt
  112. from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
  113. fig = plt.figure(figsize=(5, 5), dpi=100)
  114. ax = fig.add_subplot(111)
  115. ax.set_ylim((-1, 1)) # The sine wave never exceeds this range
  116. root = tk.Tk()
  117. # Create a canvas to draw the plot on
  118. canvas = FigureCanvasTkAgg(fig, master=root) # A Tk DrawingArea
  119. canvas.draw()
  120. canvas.get_tk_widget().grid(column=1, row=1)
  121. manager = PlotManager()
  122. manager.register("sin", sinGen.find('collector')[0], (fig, ax), LinePlot(color='red'))
  123. manager.connect('sin', 'update', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
  124. sim = Simulator(sinGen)
  125. sim.setRealTime()
  126. sim.setRealTimePlatformTk(root)
  127. sim.setDeltaT(0.1)
  128. sim.run(20.0)
  129. root.mainloop()
  130. The plot will look exactly like the one for the default platform, except that it is inside a :code:`TkInter` window
  131. now. Notice that we used the :code:`MatPlotLib` backend for visualization in :code:`TkInter`.
  132. Using Bokeh
  133. ^^^^^^^^^^^
  134. As an alternative for `MatPlotLib`, `Bokeh <https://docs.bokeh.org/en/latest/index.html>`_ kan be used. Bokeh creates
  135. a server on which you can view your plots in the browser. To launch the server use the command below. When not using
  136. this command, plots may start to "flicker" as the updates take too long.
  137. .. code-block:: bash
  138. bokeh serve <experiment file>
  139. .. note::
  140. In order to ensure that the :meth:`follow` function works for the x-axis, it is pertinent to set the
  141. :code:`x_range` attribute of the figure to the starting range. The same must be done for the y-axis.
  142. .. seealso::
  143. https://discourse.bokeh.org/t/how-to-update-x-range-y-range-in-callback/1586
  144. .. note::
  145. For a clean termination of the plot, the :code:`PlotManager.stop` method needs to be called upon termination.
  146. Otherwise, Bokeh continues to actively poll for range updates, similar to :ref:`Jupyter Notebook <jupyter>`.
  147. .. code-block:: python
  148. from CBD.realtime.plotting import PlotManager, Backend, LinePlot, follow
  149. from CBD.simulator import Simulator
  150. from bokeh.plotting import figure, curdoc
  151. sinGen = SinGen("sin")
  152. # IMPORTANT: x_range set, because this will be updated later!
  153. fig = figure(plot_width=500, plot_height=500, x_range=(0, 0), y_range=(-1, 1))
  154. document = curdoc()
  155. document.add_root(fig)
  156. # Use the Bokeh Backend
  157. manager = PlotManager(Backend.BOKEH)
  158. manager.register("sin", sinGen.find('collector')[0], fig, LinePlot(color='red'))
  159. manager.connect('sin', 'update', lambda d:
  160. manager.bokeh_set_xlim(fig, document, follow(d[0], 10.0, lower_bound=0.0)))
  161. sim = Simulator(sinGen)
  162. sim.connect("finished", manager.stop) #<< Stop polling the plots for updates
  163. sim.setRealTime()
  164. sim.setDeltaT(0.1)
  165. sim.run(20.0)
  166. .. figure:: ../_figures/sine-wave-bokeh.gif
  167. :width: 400
  168. .. warning::
  169. The simulation keeps running in the backend until the server is (requested to be) terminated. This is
  170. because `Bokeh` does not have accurate client closure hooks. Please contact the repo authors if you find
  171. a way to do this. Normally, users should not experience any issues because of this.
  172. Configuration
  173. ^^^^^^^^^^^^^
  174. The :mod:`CBD.realtime.plotting` module has a lot of configuration possibilities and options that allow
  175. a wide range of visualisations. The examples above only differ in the plotting backend, but there exist
  176. many more possibilities.
  177. Following the Signal
  178. --------------------
  179. Notice how the above examples all have a line similar to:
  180. .. code-block:: python
  181. manager.connect('sin', 'update', lambda d, a=ax: a.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
  182. This line connects a callback function that must be executed each time the :code:`sin` handler updates. In the
  183. case above, the callback function will update the x-axis limits by using the powerful
  184. :meth:`CBD.realtime.plotting.follow` method. It will follow the most recent value by using a sliding window of
  185. size 10. The signal will be kept in the center (default) and the window will not show values lower than 0.
  186. It's this function that allows the nice looking plot following. Take a look at the documentation for
  187. a detailed explanation on how this function can be used for more complex scenarios.
  188. .. note::
  189. If you want to change both axes, either group the axis update in a helper function, or connect multiple
  190. callback functions.
  191. Different Kinds of Plots
  192. ------------------------
  193. Besides the default line plot, there are some additional kinds provided. Each of these plot kinds allow
  194. configuration using the backend (keyword) arguments. These are passed to the manager during registration
  195. (notice the :code:`LinePlot` class in the code above). Simply changing this class can produce different
  196. results.
  197. .. glossary::
  198. Line Plot (:class:`CBD.realtime.plotting.LinePlot`)
  199. The most common line plot was used in the above examples. It draws a straight line between all
  200. sequential points in the given dataset.
  201. Step Plot (:class:`CBD.realtime.plotting.StepPlot`)
  202. A line plot that applies zero-order hold mechanics. Instead of drawing a straight line to the next
  203. data point, it will stay horizontal and will "jump" up stepwise.
  204. .. figure:: ../_figures/sine-wave-step.gif
  205. :width: 400
  206. Scatter Plot (:class:`CBD.realtime.plotting.ScatterPlot`)
  207. Only draws the data points, does not create a line between them.
  208. .. figure:: ../_figures/sine-wave-scatter.gif
  209. :width: 400
  210. Arrow (:class:`CBD.realtime.plotting.Arrow`)
  211. Draws an arrow vector from a given :code:`position`, with a certain :code:`size`. This uses the
  212. latest y value from the data as the (radian) angle amongst the unit circle (i.e., counter-clockwise
  213. = positive angle). The :code:`update` signal may also update the :code:`position` and the :code:`size`.
  214. .. figure:: ../_figures/arrow-wave.gif
  215. :width: 400