Skip to content

Commit

Permalink
Merge pull request Ulm-IQO#639 from Ulm-IQO/upload_improvements
Browse files Browse the repository at this point in the history
Cosmetic changes to waveform upload
  • Loading branch information
timoML committed May 6, 2021
2 parents f4218e5 + 3b83fea commit afc944e
Show file tree
Hide file tree
Showing 7 changed files with 572 additions and 3 deletions.
124 changes: 124 additions & 0 deletions core/util/benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from collections import deque
import scipy
import numpy as np
import copy


class BenchmarkTool(object):
"""
Helper that allows to benchmark a (generic) task. To this end, data of type 'quantity' vs 'time needed'
can be supplied (by querying the task). Eg. created samples vs the time needed to generate them.
Based on the gathered data, a speed value [quantity/time] or a time prediction
for a given quantity is obtained.
"""
def __init__(self, n_save_datapoints=20):
self._n_save_datapoints = n_save_datapoints
# data point: a tuple of (time [s], 'quantity')
self._datapoints = deque(maxlen=n_save_datapoints) # fifo-like
self._datapoints_fixed = list()

@property
def n_benchmarks(self):
return len(self._datapoints) + len(self._datapoints_fixed)

@property
def sanity(self):
a, t0, da = self._get_speed_fit()

if a + da < 0 or t0 < 0:
return False

return True

def reset(self):
"""
Reset all gathered data.
:return:
"""
self._datapoints_fixed = []
self._datapoints.clear()

def add_benchmark(self, time_s, y, is_persistent=False):
"""
Add a single data point to the benchmark.
:param time_s: time needed (s)
:param y: quantity
:param is_persistent: will not be cleared. If 'False' data is stored in a rolling buffer.
:return:
"""

if time_s <= 0.:
return

if not is_persistent:
self._datapoints.append((time_s, y))
else:
self._datapoints_fixed.append((time_s, y))

def estimate_time(self, y, check_sanity=True):
"""
Estimate the time needed to perform a task of given 'quantity'.
:param y: quantity
:param check_sanity: if 'True' will check sanity of the estimation
:return: time (s) to perform task, -1 if sanity check fails
"""

a, t0, _ = self._get_speed_fit()

if self.sanity or not check_sanity:
return t0 + a * y

return -1

def estimate_speed(self, check_sanity=True):
"""
Estimate the speed value from the gathered data.
:param check_sanity: if 'True' will check sanity of the estimation
:return: speed ([quantity] / s), np.nan if sanity check fails
"""
a, t0, _ = self._get_speed_fit()

if self.sanity or not check_sanity:
return 1. / a

return np.nan

def save(self, obj=None, value=None):
# function signature needs to fulfill the StatusVar logic

save_dict = copy.deepcopy(self.__dict__)
# make deque serializable
save_dict['_datapoints'] = copy.deepcopy(list(self._datapoints))

return save_dict

def load_from_dict(self, obj=None, saved_dict=None):

if saved_dict != None:
saved_dict['_datapoints'] = deque(saved_dict['_datapoints'], maxlen=self._n_save_datapoints)

self.__dict__.update(saved_dict)

def _get_speed_fit(self):

# linear fit t= a*y + t0 over all data with t: time, y: benchmark quantitiy
all_data = np.asarray(self._datapoints_fixed + list(self._datapoints))

if len(self._datapoints) > len(self._datapoints_fixed):
# ensure rolling data has max 50:50 weight
weighted_data = np.asarray(self._datapoints_fixed + list(self._datapoints)[-len(self._datapoints_fixed):])
else:
weighted_data = all_data

if len(all_data) < 1:
return np.nan, np.nan, np.nan
if len(np.unique(all_data[:,1])) == 1:
# fit needs at least 2 different datapoints in y
return np.average(all_data[:,0])/all_data[0,1], 0, np.nan
try:
a, t0, _, _, da = scipy.stats.linregress(weighted_data[:, 1], weighted_data[:, 0])
except Exception:
self.log.exception('Linear fit failed: ')
return np.nan, np.nan, np.nan

return a, t0, da
3 changes: 3 additions & 0 deletions documentation/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

Changes/New features:

* Added visual indication of ongoing waveform upload in pulsed gui.
* Added benchmarking of the upload times of pulse generator devices in pulsed gui.
* Added estimated time for waveform upload, issued for long expected upload times (threshold value configurable).
* Changed all pulser hardware to return only a dict on loading of waveform and sequences. This was previously
guaranteed by the pulser interface, but not consistently implemented.
* Added support for Keysight M8195A and M8190A AWGs.
Expand Down
89 changes: 89 additions & 0 deletions gui/pulsed/pulsed_maingui.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from gui.guibase import GUIBase
from qtpy import QtCore, QtWidgets, uic
from qtwidgets.scientific_spinbox import ScienDSpinBox, ScienSpinBox
from qtwidgets.loading_indicator import CircleLoadingIndicator
from enum import Enum


Expand Down Expand Up @@ -161,6 +162,10 @@ class PulsedMeasurementGui(GUIBase):
_ana_param_errorbars = StatusVar('ana_param_errorbars_CheckBox', False)
_predefined_methods_to_show = StatusVar('predefined_methods_to_show', [])

# signals
sigPulseGeneratorSettingsUpdated = QtCore.Signal()
sigPulseGeneratorRunBenchmark = QtCore.Signal()

def __init__(self, config, **kwargs):
super().__init__(config=config, **kwargs)

Expand Down Expand Up @@ -205,7 +210,36 @@ def on_activate(self):
self._connect_dialog_signals()
self._connect_logic_signals()

self.sigPulseGeneratorSettingsUpdated.connect(
self.pulsedmasterlogic().refresh_pulse_generator_settings,
QtCore.Qt.QueuedConnection)
self.sigPulseGeneratorRunBenchmark.connect(
self.pulsedmasterlogic().sequencegeneratorlogic().run_pg_benchmark,
QtCore.Qt.QueuedConnection)
self.sigPulseGeneratorRunBenchmark.connect(
self.benchmark_busy,
QtCore.Qt.QueuedConnection)
self.pulsedmasterlogic().sequencegeneratorlogic().sigBenchmarkComplete.connect(
self.sampling_or_loading_finished, QtCore.Qt.QueuedConnection)

self.show()

if not self.pulsedmasterlogic().sequencegeneratorlogic().has_valid_pg_benchmark():
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Benchmark missing")
dialog.setText("<center><h2>Benchmark missing:</h2></center>")
dialog.setInformativeText("Didn't find benchmark data for the pulse generator. "
"Would you like to run a benchmark now? \n \n"
"Otherwise, upload time estimation might be unavailable. "
"Make sure to close the pulsed gui gracefully to save "
"pulse generator information for future use.")
dialog.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
dialog.setDefaultButton(QtWidgets.QMessageBox.Yes)

ret = dialog.exec()
if ret == QtWidgets.QMessageBox.Yes:
self.run_pg_benchmark()

return

def on_deactivate(self):
Expand Down Expand Up @@ -234,6 +268,9 @@ def on_deactivate(self):
self._disconnect_dialog_signals()
self._disconnect_logic_signals()

self.sigPulseGeneratorSettingsUpdated.disconnect()
self.sigPulseGeneratorRunBenchmark.disconnect()

self._mw.close()
return

Expand Down Expand Up @@ -278,6 +315,7 @@ def _connect_dialog_signals(self):
self._pgs.accepted.connect(self.apply_generator_settings)
self._pgs.rejected.connect(self.keep_former_generator_settings)
self._pgs.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self.apply_generator_settings)
self._pgs.pg_benchmark.clicked.connect(self.run_pg_benchmark)

# Connect signals used in fit settings dialog
self._fsd.sigFitsUpdated.connect(self._pa.fit_param_fit_func_ComboBox.setFitFunctions)
Expand Down Expand Up @@ -389,6 +427,11 @@ def _connect_logic_signals(self):
self.pulsedmasterlogic().sigMeasurementSettingsUpdated.connect(self.measurement_settings_updated)
self.pulsedmasterlogic().sigAnalysisSettingsUpdated.connect(self.analysis_settings_updated)
self.pulsedmasterlogic().sigExtractionSettingsUpdated.connect(self.extraction_settings_updated)
self.pulsedmasterlogic().sigSampleBlockEnsemble.connect(self.sampling_or_loading_busy)
self.pulsedmasterlogic().sigLoadBlockEnsemble.connect(self.sampling_or_loading_busy)
self.pulsedmasterlogic().sigLoadSequence.connect(self.sampling_or_loading_busy)
self.pulsedmasterlogic().sigSampleSequence.connect(self.sampling_or_loading_busy)
self.pulsedmasterlogic().sigLoadedAssetUpdated.connect(self.sampling_or_loading_finished)

self.pulsedmasterlogic().sigBlockDictUpdated.connect(self.update_block_dict)
self.pulsedmasterlogic().sigEnsembleDictUpdated.connect(self.update_ensemble_dict)
Expand Down Expand Up @@ -600,6 +643,12 @@ def _setup_toolbar(self):
self._mw.current_loaded_asset_Label.setToolTip('Display the currently loaded asset.')
self._mw.control_ToolBar.addWidget(self._mw.current_loaded_asset_Label)

self._mw.loading_indicator = CircleLoadingIndicator(parent=self._mw)
self._mw.loading_indicator_action = self._mw.control_ToolBar.addWidget(
self._mw.loading_indicator
) # adding as toolbar's last item
self._mw.loading_indicator_action.setVisible(False)

self._mw.save_tag_LineEdit = QtWidgets.QLineEdit()
self._mw.save_tag_LineEdit.setMaximumWidth(200)
self._mw.save_ToolBar.addWidget(self._mw.save_tag_LineEdit)
Expand Down Expand Up @@ -701,9 +750,11 @@ def measurement_status_updated(self, is_running, is_paused):

# Enable/Disable widgets
if is_running:
# todo: not disabled on only turning pulser on without starting measurement
self._pgs.gen_use_interleave_CheckBox.setEnabled(False)
self._pgs.gen_sample_freq_DSpinBox.setEnabled(False)
self._pgs.gen_activation_config_ComboBox.setEnabled(False)
self._pgs.pg_benchmark.setEnabled(False)
for label, widget1, widget2 in self._analog_chnl_setting_widgets.values():
widget1.setEnabled(False)
widget2.setEnabled(False)
Expand Down Expand Up @@ -736,6 +787,7 @@ def measurement_status_updated(self, is_running, is_paused):
self._pgs.gen_use_interleave_CheckBox.setEnabled(True)
self._pgs.gen_sample_freq_DSpinBox.setEnabled(True)
self._pgs.gen_activation_config_ComboBox.setEnabled(True)
self._pgs.pg_benchmark.setEnabled(True)
for label, widget1, widget2 in self._analog_chnl_setting_widgets.values():
widget1.setEnabled(True)
widget2.setEnabled(True)
Expand Down Expand Up @@ -1049,6 +1101,7 @@ def keep_former_generator_settings(self):

def show_generator_settings(self):
""" Open the Pulse generator settings window. """
self.sigPulseGeneratorSettingsUpdated.emit()
self._pgs.exec_()
return

Expand Down Expand Up @@ -1464,6 +1517,11 @@ def pulse_generator_settings_updated(self, settings_dict):
self._pgs.gen_use_interleave_CheckBox.setChecked(settings_dict['interleave'])
if 'flags' in settings_dict:
self._sg.sequence_editor.set_available_flags(settings_dict['flags'])
if 'upload_speed' in settings_dict:
if np.isnan(settings_dict['upload_speed']):
settings_dict['upload_speed'] = 0.
self._pgs.upload_speed_DSpinBox.setValue(settings_dict['upload_speed'])


# unblock signals
self._pgs.gen_sample_freq_DSpinBox.blockSignals(False)
Expand Down Expand Up @@ -1883,6 +1941,32 @@ def load_ensemble_clicked(self):
self.pulsedmasterlogic().load_ensemble(ensemble_name)
return

@QtCore.Slot()
def sampling_or_loading_busy(self):
if self.pulsedmasterlogic().status_dict['sampload_busy']:
self._mw.action_run_stop.setEnabled(False)

label = self._mw.current_loaded_asset_Label
label.setText(' loading...')
self._mw.loading_indicator_action.setVisible(True)

@QtCore.Slot()
def benchmark_busy(self):
if self.pulsedmasterlogic().status_dict['benchmark_busy']:
self._mw.action_run_stop.setEnabled(False)

label = self._mw.current_loaded_asset_Label
label.setText(' benchmarking...')
self._mw.loading_indicator_action.setVisible(True)

@QtCore.Slot()
def sampling_or_loading_finished(self):
if not self.pulsedmasterlogic().status_dict['sampload_busy']:
self._mw.action_run_stop.setEnabled(True)
self._mw.loading_indicator_action.setVisible(False)



@QtCore.Slot(bool)
def generate_predefined_clicked(self, button_obj=None):
"""
Expand Down Expand Up @@ -3103,4 +3187,9 @@ def update_laser_data(self):
self.lasertrace_image.setData(x=x_data, y=y_data)
return

@QtCore.Slot()
def run_pg_benchmark(self):

self.pulsedmasterlogic().status_dict['benchmark_busy'] = True
self.sigPulseGeneratorRunBenchmark.emit()
self.sigPulseGeneratorSettingsUpdated.emit()
Loading

0 comments on commit afc944e

Please sign in to comment.