Building a Custom Measurement
Here we discuss how to build a custom Measurement component for ScopeFoundry.
We will be describing how to build the pico_datalog
measurement that is part of the demopico
tutorial package.
Basic Structure
A ScopeFoundry Measurement is defined as a sub-class of ScopeFoundry.Measurement
with a few methods (functions) defined that will describe how to setup up the measurement’s data structures (setup()
) and user interface (setup_figure()
) as well as functions to define the Measurement procedure (run()
) and display (update_display()
). We will expand on each of these functions in the next few sections.
from ScopeFoundry import Measurement
# Our measurement inherits from the ScopeFoundry Measurement class
class DemoPicoDataLogMeasurement(Measurement):
# this is the name of the measurement that ScopeFoundry uses
# when displaying your measurement and saving data related to it
name = "pico_datalog"
def setup(self):
"""
Runs once during App initialization.
This is the place to load a user interface file,
define settings, and set up data structures.
"""
def setup_figure(self):
"""
Runs once during App initialization, after setup()
This is the place to make all graphical interface initializations,
build plots, etc.
"""
def run(self):
"""
Runs when measurement is started. Runs in a separate thread from GUI.
It should not update the graphical interface directly, and should only
focus on data acquisition.
"""
def update_display(self):
"""
Displays (plots) the data
This function runs repeatedly and automatically during the measurement run.
its update frequency is defined by self.display_update_period
"""
We add this Measurement to our MicroscopeApp by the app’s add_measurement()
method:
class DemoPicoApp(BaseMicroscopeApp):
name = 'demo_pico_app'
def setup(self):
...
from demo_pico_datalog_measure import DemoPicoDataLogMeasurement
self.add_measurement(DemoPicoDataLogMeasurement(app=self))
...
Measurement Settings
We need parameters to control how the measurement is performed. These are captured in ScopeFoundry LoggedQuantity
objects that we add in the measurement’s setup()
function:
def setup(self):
# Define the logged quantities
self.settings.New("log_interval", dtype=float, initial=1.0, unit='s')
self.settings.New("log_duration", dtype=float, initial=10.0, unit='s')
self.add_operation("Setup Plot", self.setup_plot)
Run()
This is where the measurement happens!
def run(self):
# Get reference to the hardware component
hw = self.app.hardware['demo_pico']
# Create a data file to store the log
# Use the default app file save location and file naming convention
self.h5f = h5_io.h5_base_file(self.app, measurement=self)
self.h5_meas_group = h5_io.h5_create_measurement_group(self, self.h5f)
try:
# Add data array to the H5 file
self.pr_data = self.h5_meas_group.create_dataset("pr_data", (0,2), maxshape=(None,2))
start_time = time.monotonic()
elapsed_time = 0
while not self.interrupt_measurement_called and elapsed_time < self.settings['log_duration']:
current_time = time.monotonic()
elapsed_time = current_time - start_time
self.settings['progress'] = 100*elapsed_time/self.settings['log_duration']
# Read the 'pr' setting from the hardware
pr_value = hw.settings.pr.read_from_hardware()
# Log the data
new_data = [elapsed_time, pr_value]
self.pr_data.resize(self.pr_data.shape[0]+1, axis=0)
self.pr_data[-1, :] = new_data
# Sleep for the log interval
time.sleep(self.settings['log_interval'])
finally:
# Close the HDF5 file
self.h5f.close()
Define measurement steps
Saving data to disk
Data files are saved as HDF5 files. The h5_io
subpackage gives us a bunch of helper functions to make this possible
Interrupting a measurement
The user can interrupt a measurement at anytime. The run()
function should be able
Build a user interface
Load ui in setup()
loading to setup()
Bringing user interface to life
setup_figure()
and update_display()
Connect graphical widgets to code
def setup_figure(self):
...
# connect ui widgets to measurement/hardware settings or functions
self.ui.start_pushButton.clicked.connect(self.start)
self.ui.interrupt_pushButton.clicked.connect(self.interrupt)
self.settings.save_h5.connect_to_widget(self.ui.save_h5_checkBox)
self.func_gen.settings.amplitude.connect_to_widget(self.ui.amp_doubleSpinBox)
...
Create PyQtGraph plots
def setup_figure(self):
...
# Set up pyqtgraph graph_layout in the UI
self.graph_layout=pg.GraphicsLayoutWidget()
self.ui.plot_groupBox.layout().addWidget(self.graph_layout)
# Create PlotItem object (a set of axes)
self.plot = self.graph_layout.addPlot(title="Sine Wave Readout Plot")
# Create PlotDataItem object ( a scatter plot on the axes )
self.optimize_plot_line = self.plot.plot([0])
...
Update plots during run()
In order to see the data as it is aquired, an update_display()
function is called repeatedly at an interval defined by self.display_update_period
(in seconds). This value is set by default to 0.1 seconds, but can be updated in setup_figure()
.
Since we created all the plot objects during setup_figure()
this update_display()
function can be quite simple. Here we update the optimze_plot_line
using the data in self.buffer
, which is being filled by the Measurement run()
thread.
def update_display(self):
self.optimize_plot_line.setData(self.buffer)