Skip to content
Snippets Groups Projects

Draft: Added support for plotting result from many simulations. Solves #209

Open Petter Källström requested to merge MultiSimPlot into master
4 unresolved threads
Files
2
@@ -23,6 +23,7 @@ from qtpy.QtWidgets import ( # QFrame,; QScrollArea,; QLineEdit,; QSizePolicy,;
QWidget,
)
from b_asic import Simulation
from b_asic.gui_utils.icons import get_icon
from b_asic.operation import ResultKey
from b_asic.types import Num
@@ -30,13 +31,14 @@ from b_asic.types import Num
class PlotWindow(QWidget):
"""
Dialog for plotting the result of a simulation.
Dialog for plotting the result of simulations.
Parameters
----------
sim_result : dict
Simulation results of the form obtained from :attr:`~b_asic.simulation.Simulation.results`.
sfg_name : str, optional
Alternative, sim_result can be a list, ['name1', sim_result1, 'name2', sim_result2, ...]
figure_name : str, optional
The name of the SFG.
parent : optional
The parent window.
@@ -45,7 +47,7 @@ class PlotWindow(QWidget):
def __init__(
self,
sim_result: Mapping[ResultKey, Sequence[Num]],
sfg_name: Optional[str] = None,
figure_name: Optional[str] = None,
):
super().__init__()
self.setWindowFlags(
@@ -55,15 +57,67 @@ class PlotWindow(QWidget):
| Qt.WindowMaximizeButtonHint
)
title = (
f"Simulation results: {sfg_name}"
if sfg_name is not None
f"Simulation results: {figure_name}"
if figure_name is not None
else "Simulation results"
)
self.setWindowTitle(title)
self._auto_redraw = False
########### Flattening sim_result, if it is a list of results #######
# take: sim_result (possibly on form ['name1', simres1, 'name2', simres2, ...]
# generate: sim_result (dict)
if isinstance(sim_result, Simulation):
sim_result = sim_result._results
assert isinstance(sim_result, dict), TypeError(
"Parsing sim_result as a Simulation, but the _result seems broken."
)
elif isinstance(sim_result, list):
new_result = dict()
nr = 0 # value number. Used for automatic names.
name = None
for element in sim_result:
if isinstance(element, str):
assert not name, Exception(
"Parsing sim_result as a list. Did you provide two names after"
" each other?"
)
name = element
else:
if not name:
nr = nr + 1
name = "(res" + str(nr) + ")"
if isinstance(element, dict):
res = element
elif isinstance(element, Simulation):
res = element._results
assert isinstance(res, dict), TypeError(
f"Parsing sim_result as a list. Result '{name}' is a"
" Simulation, and its _result seems broken."
)
else:
raise TypeError(
"Parsing sim_result as a list. Supported results are dict"
f" and Simulation, but result '{name}' is {type(element)}"
)
for key, result in res.items():
if re.fullmatch("[0-9]+", key): # it's an output
key = "out" + key
new_result[name + "." + key] = result
name = None
assert not name, Exception(
"Parsing sim_result as a list. Did you provide a name as the last item"
" in the list?"
)
sim_result = new_result
elif not isinstance(sim_result, dict):
raise TypeError(
"sim_result must be a dict, Simulation or list."
f" Found {type(sim_result)}"
)
########### Categorizing/sorting/renaming sim_results: ##############
# take: sim_results
# take: sim_result
# generate: key_order, initially_checked
# generate: updated_result
initially_checked = []
@@ -72,12 +126,16 @@ class PlotWindow(QWidget):
n = 0
for key, result in sim_result.items():
key2 = key # in most cases
if re.fullmatch("in[0-9]+", key):
if re.fullmatch(r"(.*\.)?in[0-9]+", key):
m = 4
elif re.fullmatch("[0-9]+", key):
elif re.fullmatch(r"[0-9]+", key): # It's an output as just a number.
m = 3
key2 = 'out' + key
elif re.fullmatch(
r"(.*\.)?out[0-9]+", key
): # It's an output already formulated.
m = 3
key2 = 'o' + key
elif re.fullmatch("t[0-9]+", key):
elif re.fullmatch(r"(.*\.)?t[0-9]+", key):
m = 2
else:
m = 1
@@ -130,8 +188,16 @@ class PlotWindow(QWidget):
self._plot_axes.xaxis.set_major_locator(MaxNLocator(integer=True))
self._lines = {}
markers = ".ov<^>s+*xd|_"
fmt = '-'
ix = 0 # index bytes in markers
for key in key_order:
line = self._plot_axes.plot(updated_result[key], label=key)
if len(updated_result[key]) <= 100:
fmt = markers[ix] + '-'
ix = (ix + 1) % len(markers)
else:
fmt = '-'
line = self._plot_axes.plot(updated_result[key], fmt, label=key)
self._lines[key] = line[0]
self._plot_canvas = FigureCanvas(self._plot_fig)
@@ -172,8 +238,8 @@ class PlotWindow(QWidget):
# Add additional checkboxes
self._legend = self._plot_axes.legend()
self._legend_checkbox = QCheckBox("&Legend")
self._legend_checkbox.stateChanged.connect(self._legend_checkbox_change)
self._legend_checkbox.setCheckState(Qt.CheckState.Checked)
self._legend_checkbox.stateChanged.connect(self._legend_checkbox_change)
self._legend_checkbox.setIcon(get_icon('legend'))
listlayout.addWidget(self._legend_checkbox)
# self.ontop_checkbox = QCheckBox("&On top")
@@ -195,12 +261,60 @@ class PlotWindow(QWidget):
# self.plotcanvas.draw()
self._auto_redraw = True
def add_result(self, name, result) -> None:
"""
Add another result to the plot.
The new signals are added in natural order without sorting them.
---
name: str
The name of the result
result: dict
The result from one simulation.
"""
markers = ".ov<^>s+*xd|_"
ix = 0
self._auto_redraw = False
def addline(key, vector, checked):
line = self._plot_axes.plot(np.real(res), fmt, label=key)
self._lines[key] = line[0]
list_item = QListWidgetItem(key)
list_item.setCheckState(Qt.CheckState.Unchecked) # will add it if checked
self._checklist.addItem(list_item)
self._lines[key].remove() # remove the line from plot. Keep it in _lines.
list_item.setCheckState(checked) # will add it if checked
for key, res in result.items():
key2 = name + "." + key
ischecked = Qt.CheckState.Unchecked
if re.fullmatch(r"[0-9]+", key):
key2 = name + '.out' + key
ischecked = Qt.CheckState.Checked
if len(res) <= 100:
fmt = markers[ix] + '-'
ix = (ix + 1) % len(markers)
else:
fmt = '-'
if all(np.imag(np.real_if_close(res)) == 0):
# real: add one line with corresponding checkbox
addline(key2, np.real(res), ischecked)
else:
# complex: add '_re', '_im', '_mag', '_ang'
addline(key2 + "_re", np.real(res), ischecked)
addline(key2 + "_im", np.imag(res), ischecked)
addline(key2 + "_mag", np.absolute(res), Qt.CheckState.Unchecked)
addline(key2 + "_ang", np.angle(res), Qt.CheckState.Unchecked)
self._auto_redraw = True
self._update_legend()
self._plot_canvas.draw()
def _legend_checkbox_change(self, check_state):
self._legend.set(visible=(check_state == Qt.CheckState.Checked))
if self._auto_redraw:
if check_state == Qt.CheckState.Checked:
self._legend = self._plot_axes.legend()
self._plot_canvas.draw()
self._update_legend()
self._plot_canvas.draw()
# def _ontop_checkbox_change(self, checkState):
# Bugg: It seems the window closes if you change the WindowStaysOnTopHint.
@@ -215,10 +329,12 @@ class PlotWindow(QWidget):
self._checklist.item(x).setCheckState(Qt.CheckState.Checked)
self._auto_redraw = True
self._update_legend()
self._plot_canvas.draw()
def _update_legend(self):
self._legend = self._plot_axes.legend()
self._plot_canvas.draw()
# if self._legend_checkbox.checkState == Qt.CheckState.Checked:
if self._legend_checkbox.isChecked():
self._legend = self._plot_axes.legend()
def _button_none_click(self, event):
self._auto_redraw = False
@@ -226,6 +342,7 @@ class PlotWindow(QWidget):
self._checklist.item(x).setCheckState(Qt.CheckState.Unchecked)
self._auto_redraw = True
self._update_legend()
self._plot_canvas.draw()
def _item_change(self, listitem):
key = listitem.text()
@@ -235,6 +352,7 @@ class PlotWindow(QWidget):
self._lines[key].remove()
if self._auto_redraw:
self._update_legend()
self._plot_canvas.draw()
def _relim(self, event=None):
self._plot_axes.relim(True)
@@ -247,6 +365,16 @@ class PlotWindow(QWidget):
def start_simulation_dialog(
sim_results: Dict[str, List[complex]], sfg_name: Optional[str] = None
):
"""
deprecated::
Use :func:`show_simulation_result` instead.
"""
show_simulation_result(sim_results, sfg_name)
def show_simulation_result(
sim_results: Dict[str, List[complex]], figure_name: Optional[str] = None
) -> PlotWindow:
"""
Display the simulation results window.
@@ -254,21 +382,49 @@ def start_simulation_dialog(
----------
sim_results : dict
Simulation results of the form obtained from :attr:`~b_asic.simulation.Simulation.results`.
sfg_name : str, optional
Alternative, sim_result can be a list, ['name1', sim_result1, 'name2', sim_result2, ...]
figure_name : str, optional
The name of the SFG.
Note: This starts the GUI event loop using `QApplication.instance().exec()`, and the function
will return when the window is closed.
If this is not desired, you can in ipython start the event loop using `%gui qt`, and define
a global variable `noqtexec = 1`.
Effect if no event loop is running: The windows turns black and do not reponse.
Effect if the event loop is started twice: The function will never return.
"""
if not QApplication.instance():
app = QApplication(sys.argv)
else:
app = QApplication.instance()
win = PlotWindow(sim_result=sim_results, sfg_name=sfg_name)
win = PlotWindow(sim_result=sim_results, figure_name=figure_name)
win.show()
app.exec_()
# I did not get "if 'noqtexec' in globals()" to work. Try this instead:
try:
if noqtexec != 1:
raise NameError()
except NameError:
app.exec()
return win
# Simple test of the dialog
from time import sleep
if __name__ == "__main__":
sim_res = {
res1 = {
'0': [1.5, 1.6, 1.5, 1],
'1': [1.0, 2.0, 1.5, 1.1],
'add1': [1.5, 1.5, 1, 1],
'cmul1': [1, 1.5, 1, 1],
'cmul2': [1.5, 1, 1, 1],
'in1': [2, 1, 1, 1],
'in2': [1.1, 3, 1, 1],
't1': [1, 2, 1, 1],
't2': [1, 1, 2, 1],
't3': [1, 1, 1, 2],
}
res2 = {
'0': [0.5, 0.6, 0.5, 0],
'1': [0.0, 1.0 + 0.3j, 0.5, 0.1j],
'add1': [0.5, 0.5, 0, 0],
@@ -280,4 +436,19 @@ if __name__ == "__main__":
't2': [0, 0, 1, 0],
't3': [0, 0, 0, 1],
}
start_simulation_dialog(sim_res, "Test data")
res3 = {
'0': np.random.rand(50),
'1': np.random.rand(200).tolist(),
}
# res4 = {
# '0': np.random.rand(60).tolist(),
# '1': np.random.rand(220).tolist(),
# 't4': np.random.rand(50).tolist(),
# }
win = show_simulation_result(['r1', res1, res2])
try:
if noqtexec == 1:
sleep(5)
win.add_result('hej', res3)
except NameError:
pass
Loading