Source code for GUI.CustomWidgets.LivePlots
import pyqtgraph
from setup import Setup
from PyQt5.QtCore import QTimer
from typing import Tuple
import numpy
import logging
logger = logging.getLogger("root")
[docs]class LivePlotSignal(object):
"""
A LivePlotSignal stores all the information needed to identify and plot a single signal.
:type name: str
:param name: Name of the signal, to be displayed on the legend of the plot the signal is shown on
:type identifier: str
:param identifier: Identifier of the signal, used to retrieve the signal from the measurement buffer of the setup
:type color: str
:param color: Color of the plotted line used to instantiate the corresponding pen
:type width: float
:param width: Width of the plotted line used to instantiate the corresponding pen
.. note::
Selecting integer values for the width parameter results in smoother plots.
"""
def __init__(self, name: str, identifier: str, color: str, width=1):
self.name = name
self.identifier = identifier
self.pen = pyqtgraph.mkPen(color=color, width=width)
self.data_line = None
[docs]class LivePlotWidget(pyqtgraph.PlotWidget):
"""
The LivePlotWidget makes use of pyqtgraph to allow plotting a number of signals. It automatically updates.
:type setup: Setup
:param setup: Instance of the current setup to allow access to the measurement buffer
:type title: str
:param title: Title of the plot
:type ylabel: str
:param ylabel: Label of the y-axis
:type ylims: Tuple
:param ylims: Limits of the y-axis
"""
def __init__(
self, setup: Setup, title: str, ylabel: str, ylims: Tuple, *args, **kwargs
) -> None:
super(LivePlotWidget, self).__init__(*args, **kwargs)
self.setup = setup
self.signals = []
self.ylims = ylims
self.title = title
# Standard visual setup for plots:
self.showGrid(x=True, y=True, alpha=0.7)
self.setBackground("w")
self.setXRange(0, self.setup.interval_s)
self.setYRange(ylims[0], ylims[1])
self.setTitle(title)
self.setLabel("bottom", "Time [s]")
self.setLabel("left", ylabel)
# Set up timer for live plotting
self.timer = QTimer()
self.timer.setInterval(50)
self.timer.timeout.connect(self.update_plot_data)
self.timer.start()
[docs] def add_signals(self, signals: list) -> None:
"""
Add a list of signals to the plot.
:type signals: list
:param signals: List of LivePlotSignals
"""
self.addLegend()
for signal in signals:
# Create a line in the plot
signal.data_line = pyqtgraph.PlotCurveItem(
[], [], pen=signal.pen, name=signal.name
)
# Add the signal
self.signals.append(signal)
self.addItem(signal.data_line)
[docs] def update_plot_data(self):
"""
Handles the updating of a LivePLotWidget.
"""
if self.setup.measurement_buffer["Time"]:
if self.signals:
shifted_time_axis = (
numpy.array(self.setup.measurement_buffer["Time"])
- self.setup.measurement_buffer["Time"][-1]
+ self.setup.interval_s
)
n_entries = len(shifted_time_axis)
for signal in self.signals:
signal.data_line.setData(
numpy.asarray(shifted_time_axis).flatten(),
numpy.asarray(
self.setup.measurement_buffer[signal.identifier]
).flatten()[0:n_entries],
)
else:
# No signals added yet
pass
else:
# No signals recorded yet
pass
[docs] def reset_plot_layout(self) -> None:
"""
Allows to reset the plot layout to the original view
"""
if self.ylims is not None:
self.setYRange(self.ylims[0], self.ylims[1])
else:
logger.error(
"Use set_ylims to define y-axis limits before reseting the plot layout for plot {}".format(
self.title
)
)
self.setXRange(0, self.setup.interval_s)
[docs]class LivePlotWidgetCompetition(LivePlotWidget):
"""
Specialized LivePLotWidget allowing only two signals and adding color between the two corresponding lines.
Used to visualize the integral of the control error.
"""
def __init__(self, setup: Setup, title, ylabel, ylims, *args, **kwargs) -> None:
super(LivePlotWidgetCompetition, self).__init__(
setup=setup, title=title, ylabel=ylabel, ylims=ylims, *args, **kwargs
)
self.fill_between = None
[docs] def add_signals(self, reference_signal, actual_signal):
self.addLegend()
reference_signal.data_line = pyqtgraph.PlotCurveItem(
[], [], pen=reference_signal.pen, name=reference_signal.name
)
actual_signal.data_line = pyqtgraph.PlotCurveItem(
[], [], pen=actual_signal.pen, name=actual_signal.name
)
brush = pyqtgraph.mkBrush(color=(255, 0, 0, 50))
self.fill_between = pyqtgraph.FillBetweenItem(
curve1=reference_signal.data_line,
curve2=actual_signal.data_line,
brush=brush,
)
self.signals.append(reference_signal)
self.signals.append(actual_signal)
self.addItem(actual_signal.data_line)
self.addItem(reference_signal.data_line)
self.addItem(self.fill_between)
[docs] def update_plot_data(self):
if self.setup.measurement_buffer["Time"]:
if self.signals:
shifted_time_axis = (
numpy.array(self.setup.measurement_buffer["Time"])
- self.setup.measurement_buffer["Time"][-1]
+ self.setup.interval_s
)
n_entries = len(shifted_time_axis)
for signal in self.signals:
signal.data_line.setData(
numpy.asarray(shifted_time_axis).flatten(),
numpy.asarray(
self.setup.measurement_buffer[signal.identifier]
).flatten()[0:n_entries],
)
self.fill_between.setCurves(
curve1=self.signals[0].data_line, curve2=self.signals[1].data_line
)
[docs]class PlotWidgetFactory:
"""
The PlotWidgetFactory defines a simple interface for creating instances of previously defined LivePlotWidgets.
"""
def __init__(self, setup):
self.setup = setup
def delta_t(self):
graph_delta_t = LivePlotWidget(
setup=self.setup,
title="Temperature Difference",
ylabel="Temperature Difference [°C]",
ylims=(0, self.setup.config['general']['temperature_difference_set_point_high'] + 5),
)
signal_actual_delta_t = LivePlotSignal(
name="Actual Delta T", identifier="Temperature_Difference", color="b"
)
signal_target_delta_t = LivePlotSignal(
name="Target Delta T", identifier="Target_Delta_T", color="r"
)
graph_delta_t.add_signals([signal_actual_delta_t, signal_target_delta_t])
return graph_delta_t
def temperatures(self):
graph_temperatures = LivePlotWidget(
setup=self.setup,
title="Temperatures",
ylabel="Temperature [°C]°",
ylims=(20, 20 + self.setup.config['general']['temperature_difference_set_point_high'] + 5),
)
signal_temperature_one = LivePlotSignal(
name="Temperature 1", identifier="Temperature_1", color="b"
)
signal_temperature_two = LivePlotSignal(
name="Temperature 2", identifier="Temperature_2", color="r"
)
graph_temperatures.add_signals([signal_temperature_one, signal_temperature_two])
return graph_temperatures
def flow(self):
graph_flow = LivePlotWidget(
setup=self.setup, title="Flow", ylabel="Flow [slm]", ylims=(0, 100)
)
signal_flow = LivePlotSignal(
name="Flow Measurement", identifier="Flow", color="b"
)
signal_flow_estimate = LivePlotSignal(
name="Flow Estimate", identifier="Flow_Estimate", color="r"
)
graph_flow.add_signals([signal_flow, signal_flow_estimate])
return graph_flow
def pid(self):
graph_pid = LivePlotWidget(
setup=self.setup, title="PID Components", ylabel="Gain", ylims=(-0.5, 2.5)
)
singal_p = LivePlotSignal(name="P", identifier="Controller_Output_P", color="r")
signal_i = LivePlotSignal(name="I", identifier="Controller_Output_I", color="g")
signal_d = LivePlotSignal(name="D", identifier="Controller_Output_D", color="b")
signal_pid = LivePlotSignal(
name="PID", identifier="Controller_Output", color="k", width=2
)
graph_pid.add_signals([singal_p, signal_i, signal_d, signal_pid])
return graph_pid
def power(self):
graph_power = LivePlotWidget(
setup=self.setup, title="Power", ylabel="Power [%]", ylims=(0, 1)
)
signal_power = LivePlotSignal(name="Power", identifier="PWM", color="k")
graph_power.add_signals([signal_power])
return graph_power
def delta_t_competition(self):
graph_delta_t = LivePlotWidgetCompetition(
setup=self.setup,
title="Temperature Difference",
ylabel="Temperature Difference [°C]",
ylims=(0, self.setup.config['general']['temperature_difference_set_point_high'] + 5),
)
signal_actual_delta_t = LivePlotSignal(
name="Actual Delta T", identifier="Temperature_Difference", color="b"
)
signal_target_delta_t = LivePlotSignal(
name="Target Delta T", identifier="Target_Delta_T", color="r"
)
graph_delta_t.add_signals(
actual_signal=signal_actual_delta_t, reference_signal=signal_target_delta_t
)
return graph_delta_t