Settings and LoggedQuantity
The ubiquitous usage of settings is encouraged as:
- They synchronize their values with GUI widgets, hardware devices, and other settings.
- It is also easy to save settings for later use, hence they capture state of the system.
This text aims to clarify the difference of settings
and LoggedQuantity
as well as give an overview of their usage.
Creation of settings
The following example shows the creation of a new setting “X” within a hardware component called “example”, which will be used throughout this text:
from ScopeFoundry import HardwareComponent
class Example(HardwareComponent):
name = "example"
def setup(self) -> None:
lq = self.settings.New("X", dtype=float, initial=1.0)
Apart from creating a {name, value} pair, we created an object of type ScopeFoundry.LoggedQuantity
(LQ). This particular LQ holds a value of data type float
. The LQ created here is part of a collection of LQs called settings
.
Learn to master setting creation here.
Nomenclature
LoggedQuantity
or LQ, is an object that holds a value. LQs have convenience methods to synchronize their values across plugins, threads, to devices and to the UI.settings
is a collection of LQs. EveryMeasurement
, everyHardwareComponent
, and theBaseMicroscopeApp
has asettings
attribute.settings
is of typeLQCollection
, which provide convenience functions to create new LQs and generate UI elements.- On this website, the term setting dependents on the context. Often just refers to a {name, value} pair that is implemented as an
LQ
and being part of asettings
.
Reference a LoggedQuantity
To access the LQ, there are three options depending on your current scope:
At creation
lq = self.settings.New("X", float, initial=1.0)
Learn to master setting creation here.
Within the same class/plugin
lq = self.settings.get_lq("X")
# equivalent to
# lq = self.settings.X
Global reference with plug-in
To reference an LQ from another plug-in use:
example_hw = self.app.hardware.example
lq = example_hw.settings.get_lq("X")
The first line references the plugin that holds the settings with the desired LoggedQuantity.
As in the section above, we use the get_lq
method on the settings container.
Global reference with lq_paths
If we just want the LQ from another plugin, the above lines are equivalent.
lq = self.app.get_lq("hw/example/X")
Note that we used an lq_path, here “hw/example/X”. Lq_paths have the form:
- “hw/hardware_name/setting_name” for hardware
- “mm/measurement_name/setting_name” for measurement,
- “app/setting_name" for app level.
Retrieve and Change the Value
It is straightforward to change the value of an LQ; there exist two notations.
From the
settings
container: Use dictionary-style notation to access and change the value ofX
.# Retrieve value val = self.settings["X"] print(f"X has value {val}") # Outputs the current value of X # Set value self.settings["X"] = 2.0 val = self.settings["X"] print(f"X has value {val}") # Outputs 2.0
From an LQ:
lq = self.settings.get_lq("X") # Retrieve value val = lq.val print(f"X has value {val}") # Outputs the current value of X # Set value lq.update_value(2.0) print(f"X has value {val}") # Outputs 2.0
If the LQ has a hardware read function (see #Sync with a Hardware Device), then you can read the value from the hardware, update the LQ’s value, and retrieve the value using the line:
val = lq.read_from_hardware()
Syncing LQ with Widgets
Assume you have already created a QtWidget my_spin_box
of type QtWidgets.QDoubleSpinBox
. You can keep the value displayed by the spin box and the value of the setting synchronized using:
# my_spin_box = QtWidgets.QDoubleSpinBox()
lq.connect_to_widget(my_spin_box)
Experimental in ScopeFoundry 2.1: disconnect_from_widget.
lq.disconnect_from_widget(my_spin_box)
Creation of and Connection to Default Widgets
Every LQ has a default widget. So, you can create and connect to a default widget in one line:
my_spin_box = lq.new_default_widget()
Note, the dtype
determines the default widget—here, a float
’s default is a QDoubleSpinBox. Not every widget type can be connected to every dtype. In practice, you can create multiple such widgets in one line.
widget_with_spinbox_and_more = self.settings.New_UI(include=("X", ...))
Learn more to create collection widgets here here.
Learn to tune the default widget here.
Syncing LQs with a Hardware Device
We illustrate this by expanding the example above with a connect
method:
from ScopeFoundry import Hardware
class Example(Hardware):
name = "example"
def setup(self) -> None:
lq = self.settings.New("X", dtype=float, initial=1.0)
def connect(self):
self.settings.get_lq("X").connect_to_hardware(read_func, write_func)
Here, read_func
and write_func
are functions that implement communication with the device.
Now, every time the setting’s value is changed, the write_func
is called to write the value to the device.
However, reading the value from the device needs to be called explicitly. From any plugin, this can be done with the following line:
val = self.app.get_lq("hw/example/X").read_from_hardware()
Where val
can be used immediately.
Note that for one-way synchronization you can set either read_func
or write_func
to None
. See also here.
Syncing LQs with Each Other
Single Variable Dependence
To bidirectionally link two LQs, you need to define two functions and pass them with the co-dependent LQ to the connect_lq_math
method. If only one-way updates are required, skip defining and passing the reverse function.
For example, syncing two LQs “radius” and “diameter”:
r = self.settings.New("radius")
d = self.settings.New("diameter")
def to_radius(d):
return d / 2.0
# reverse function
def to_diameter(r):
return r * 2.0
r.connect_lq_math(d, to_radius, to_diameter)
For the special case of linear co-dependence, you can use the built-in method connect_lq_scale
. The above code is equivalent to:
r = self.settings.New("radius")
d = self.settings.New("diameter")
r.connect_lq_scale(d, 0.5)
Multi-Variable Dependence
For example, the volume of a cylinder as a function of its height and radius:
r = self.settings.New("radius")
h = self.settings.New("height")
v = self.settings.New("cylinder_volume")
def to_cylinder_volume(r, h):
return 3.1415 * r**2 * h
# reverse function
def to_radius_and_height(v):
# leaving r unchanged is an arbitrary choice.
r = self.settings["radius"]
return r, (v / (3.1415 * r ** (1 / 2)))
v.connect_lq_math((r, h), to_cylinder_volume, to_radius_and_height)
INFO: ScopeFoundry has a more comprehensive implementation of this example, see documentation here.
Advanced: LQCircularNetwork
As an example, defining a range ‘X’ with four settings:
X_start
X_stop
X_range
X_center
clearly has redundant information. If X_stop
is increased by the user, then both X_range
and X_center
should update. The reverse is also true; if the X_range
changes, then X_start
and X_stop
are updated. This circular quality of interdependence would result in infinite recursion, with each setting updating the others.
To break this recursion, subclass ScopeFoundry.logged_quantity.lq_circular_network.LQCircularNetwork
and use the method update_values_synchronously
in your listener methods.
An implementation of the above example would be:
from ScopeFoundry.logged_quantity import LoggedQuantity
from ScopeFoundry.logged_quantity.lq_circular_network import LQCircularNetwork
class Interval(LQCircularNetwork):
def __init__(
self,
min_lq: LoggedQuantity,
max_lq: LoggedQuantity,
center_lq: LoggedQuantity,
span_lq: LoggedQuantity,
):
self.min = min_lq
self.max = max_lq
self.center = center_lq
self.span = span_lq
lq_dict = {
"min": self.min,
"max": self.max,
"center": self.center,
"span": self.span,
}
LQCircularNetwork.__init__(self, lq_dict)
self.min.add_listener(self.on_change_min_max)
self.max.add_listener(self.on_change_min_max)
self.center.add_listener(self.on_change_center_span)
self.span.add_listener(self.on_change_center_span)
def on_change_min_max(self):
mn = self.min.val
mx = self.max.val
span = abs(mx - mn)
center = mn + span / 2.0
self.update_values_synchronously(span=span, center=center)
def on_change_center_span(self):
span = self.span.val
center = self.center.val
mn = center - span / 2.0
mx = center + span / 2.0
self.update_values_synchronously(min=mn, max=mx)
INFO: ScopeFoundry has a more comprehensive implementation of this example, see documentation here.