LivePlot.rst 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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.CBD 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
  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. manager = PlotManager()
  58. manager.register("sin", sinGen.findBlock('collector')[0], (fig, ax), LinePlot(color='red'))
  59. manager.connect('sin', 'update_event', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
  60. sim = Simulator(sinGen)
  61. sim.setRealTime()
  62. sim.setDeltaT(0.1)
  63. sim.run(20.0)
  64. plt.show()
  65. .. figure:: ../_figures/sine-wave-mpl.gif
  66. :width: 400
  67. Seaborn
  68. ^^^^^^^
  69. `Seaborn <https://seaborn.pydata.org/>`_ is a data visualization library, built on top of `MatPlotLib`.
  70. Hence, it can be easily integrated and used for plotting live data. It can simply be used by providing
  71. the :code:`PlotManager`'s constructor with a backend argument:
  72. .. code-block:: python
  73. manager = PlotManager(Backend.SNS)
  74. That's it. To change the theme to a `Seaborn` theme, you can either
  75. `use a MatPlotLib theme <https://matplotlib.org/stable/gallery/style_sheets/style_sheets_reference.html>`_ theme,
  76. or place the following code before the creation of the figure (see also
  77. `Seaborn's documentation <https://seaborn.pydata.org/generated/seaborn.set_theme.html#seaborn.set_theme>`_ on
  78. this topic):
  79. .. code-block:: python
  80. import seaborn as sns
  81. sns.set_theme(style="darkgrid") # or any of darkgrid, whitegrid, dark, white, ticks
  82. Jupyter Notebook
  83. ^^^^^^^^^^^^^^^^
  84. These days, `Jupyter Notebooks <https://jupyter.org/>`_ are the most common way to collect experiments.
  85. Luckily, the :class:`CBD.realtime.plotting.PlotManager` can work with them without too much overhead. In fact,
  86. all that's required is setting the magic function :code:`%matplotlib` **before** creating the plot. That's it!
  87. However, a small caveat is the fact that a :code:`notebook` stays alive after the simulation finishes. This
  88. means the :code:`PlotManager` keeps polling for data. To stop this, connect a signal that terminates this
  89. polling to the simulator **before** starting the simulation:
  90. .. code-block:: python
  91. # Kills all polling requests and closes the plots
  92. sim.connect("finished", manager.terminate)
  93. # Kills all polling requests, but keeps plots alive
  94. sim.connect("finished", manager.stop)
  95. Also take a look at the :code:`examples/notebook` folder for more info.
  96. TkInter
  97. ^^^^^^^
  98. Now, as mentioned in :doc:`RealTime`, there is also a :code:`TkInter` platform to run the realtime
  99. simulation on. This can be useful for creating graphical user interfaces (GUIs). Sometimes, such a
  100. GUI might be in need of a plot of the data. See also the :doc:`Dashboard` example for a more complex
  101. variation.
  102. .. code-block:: python
  103. from CBD.realtime.plotting import PlotManager, LinePlot, follow
  104. from CBD.simulator import Simulator
  105. import tkinter as tk
  106. import matplotlib.pyplot as plt
  107. from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
  108. fig = plt.figure(figsize=(5, 5), dpi=100)
  109. ax = fig.add_subplot(111)
  110. ax.set_ylim((-1, 1)) # The sine wave never exceeds this range
  111. root = tk.Tk()
  112. # Create a canvas to draw the plot on
  113. canvas = FigureCanvasTkAgg(fig, master=root) # A Tk DrawingArea
  114. canvas.draw()
  115. canvas.get_tk_widget().grid(column=1, row=1)
  116. manager = PlotManager()
  117. manager.register("sin", sinGen.findBlock('collector')[0], (fig, ax), LinePlot(color='red'))
  118. manager.connect('sin', 'update_event', lambda d, axis=ax: axis.set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
  119. sim = Simulator(sinGen)
  120. sim.setRealTime()
  121. sim.setRealTimePlatformTk(root)
  122. sim.setDeltaT(0.1)
  123. sim.run(20.0)
  124. root.mainloop()
  125. The plot will look exactly like the one for the default platform, except that it is inside a :code:`TkInter` window
  126. now.
  127. Using Bokeh
  128. -----------
  129. As an alternative for `MatPlotLib`, `Bokeh <https://docs.bokeh.org/en/latest/index.html>`_ kan be used. However, as
  130. you will see, this will require a little bit more "managing" code.
  131. .. attention::
  132. While functional for the most part, live plotting using `Bokeh` is still in beta. Not all features will work
  133. as expected.
  134. .. warning::
  135. In order to get this plotting framework to show live plots, you need to start a `Bokeh` server via the command:
  136. .. code-block:: bash
  137. bokeh serve
  138. |
  139. .. code-block:: python
  140. from CBD.realtime.plotting import PlotManager, Backend, LinePlot, follow
  141. from CBD.simulator import Simulator
  142. from bokeh.plotting import figure, curdoc
  143. from bokeh.client import push_session
  144. fig = figure(plot_width=500, plot_height=500, y_range=(-1, 1))
  145. curdoc().add_root(fig)
  146. # Use the Bokeh Backend
  147. manager = PlotManager(Backend.BOKEH)
  148. manager.register("sin", sinGen.findBlock('collector')[0], fig, LinePlot(color='red'))
  149. def set_xlim(limits):
  150. lower, upper = limits
  151. fig.x_range.start = lower
  152. fig.x_range.end = upper
  153. manager.connect('sin', 'update_event', lambda d: set_xlim(follow(d[0], 10.0, lower_bound=0.0)))
  154. session = push_session(curdoc())
  155. session.show()
  156. sim = Simulator(sinGen)
  157. sim.setRealTime()
  158. sim.setDeltaT(0.1)
  159. sim.run(20.0)
  160. # NOTE: currently, there can be 'flickering' of the plot
  161. import time
  162. while manager.is_opened():
  163. session.push()
  164. time.sleep(0.1)
  165. .. figure:: ../_figures/sine-wave-bokeh.gif
  166. :width: 400
  167. .. note::
  168. Currenly, there is a lot of "flickering" of the plot. There has not yet been found a solution
  169. for this problem. It is presumed that this is a consequence of Bokeh being browser-based.