Support dynamic and settable Facet limits
@jondoesntgit and I discussed this topic this morning. It would be nice to support Facet limits that don't have to be hard-coded at the class level. In particular, I ran into this in relation to the pixelclock facet in UC480_Camera. The valid limits can change depending on other camera state, so we can't simply hard-code them or load them once at startup.
It also would be nice to support limits that can be set by the user. In this case, the limits would likely be per-instance, meaning they should probably go within a FacetInstance.
One idea is to allow passing a callable for the limits, either one callable for all of the limits, or one per limit (start, stop, step). Probably just one callable. The callable would take both the instrument instance and the Facet as parameters, e.g. limit(obj, facet). That way you could access the instrument's FacetInstance easily and use it to store data, e.g. return facet.instance(obj).limits.
That sounds like a reasonable approach for reading out the dynamic limits. How the user sets the limits is a more challenging question.
- Should we validate user-set limits? Probably.
- How are the limits accessed? Do we simply have instance attributes (
ps.voltage_limits) (and are they public or private?), or namespace them all under another object (ps.limits.voltage)? - Do we instead give easier access to the
FacetInstanceitself, and allow setting limits (and possibly other per-instance data) through methods ofFacetInstance? e.g.ps._voltage_.set_limits(x,y,z)orps.facets.voltage.set_limits(x,y,z)orps.facets.voltage.limits = (x,y,z). This would requireFacetInstanceto add explicit support for things like limits if we want validation.
A quick primer on FacetInstance: Facets are just like properties, and exist at the class (not instance) level. That means per-instance state, if required, must be kept through another object. To do this, a Facet will stash a FacetInstance inside an instrument's __dict__ attribute, under it's own name. This FacetInstance can be accessed (and is auto-created) by using the Facet's instance() method. For example, if you have a UC480_Camera, you can get the FacetInstance for its pixelclock via cam.__class__.pixelclock.instance(cam).
A very bare-bones example that only utilizes the callable-limit ability might look like this:
class MyPowerSupply(Instrument):
def _get_voltage_limits(self, facet):
try:
return obj._voltage_limits
except AttributeError:
return (0, 10, None) # Default limits
voltage = ManualFacet(limits=_get_voltage_limits)
ps = MyPowerSupply()
ps._voltage_limits = (0, 5, None) # Set more conservative limit
ps.voltage = '9 volts' # Should cause an error
You could make this more ergonomic for reuse by making a constructor function:
def my_facet(default_limits, **kwds):
def get_limits(obj, facet):
return getattr(obj, '_' + facet.name + '_limits', default_limits)
return ManualFacet(limits=get_limits, **kwds)
class MyPowerSupply(Instrument):
voltage = my_facet(default_limits=(0, 10, None), units='volt')
current = my_facet(default_limits=(0, 2, None), units='amps')
I like the idea of using a callable for the limits. It is a lot like the "validators" used in pymeasure, which I think would be a nice way to add flexible bounds checking to facets: https://github.com/pymeasure/pymeasure/blob/master/pymeasure/instruments/validators.py.
I haven't dug into instrumental yet, but it seems like facets are a similar concept to the "control", "measurement" and "setting" methods of the Instrument class in pymeasure (?): https://github.com/pymeasure/pymeasure/blob/44b453e65f533bc8ad4499df26f8ad61607faf6a/pymeasure/instruments/instrument.py#L37
Perhaps there could be class-level limits that cover the maximum range of the instrument, and then user defined, per-instance, limits that must fall within the class level limits. Then there could be a callable "validator" that checks for user defined limits first, but defaults to the class level limits if non are found. One would also have to consider handling limits that are set directly on the insturment, not thorough Instrumental; i.e. someone setting the voltage limits of a PSU directly via the front panel.