Instrumental
Instrumental copied to clipboard
Mock Instruments
Hey,
I would like to write a functionality to mock some instruments based on this framework. Something akin to what QCoDeS refers to as a Manual Instrument. Would you be willing to merge this in if I wrote it?
Jonathan
Would this require something similar to the VisaMixin class in instrumental.drivers.__init__.py
?
A mixin class might be the right way to define mock/manual instruments, though it might be enough to define some kind of MockFacet
.
Could you give an example of what you'd like to achieve/how you'd use such an instrument, and what features (methods/behaviors) you'd like it to support? That'll give me a better idea of which approach would be better.
Sure.
Implementation A
from instrumental import mock_instrument
power_supply = mock_instrument()
power_supply.voltage = MockFacet(units=ureg.volts)
power_supply.current = MockFacet(units=ureg.milliamps)
... walk over in the lab to tweak some settings
power_supply.voltage = ureg.Quantity('1.2 volt')
power_supply.current = ureg.Quantity('20 milliamps')
Implementation B
// mock_instruments.json
{
"name":"power_supply",
"facets": [
{"name": "voltage", "units": "volts"},
{"name": "current", "units": "milliamps"}
]
}
power_supply = load_instrument('power_supply')
power_supply.voltage = ureg.Quantity('1.2 volt')
power_supply.current = ureg.Quantity('20 milliamps')
... And then of course, you'd be able to save and load the instrument as you normally do for other instruments. I haven't looked into the actual file format you use for serializing saved instruments...
Here's an example implementation.
Suppose that we have some laser diode, that is driven by a laser diode driver. The laser diode itself is a tiny 50-gram piece of equipment that sits in a larger chassis that delivers power to it from the driver. The laser's output, linewidth, wavelength, etc... depend on the the current it receives from the laser diode driver as well as its temperature.
Often, we are more concerned about getting a specific power out of the laser than knowing its drive current. One temptation would be to put a helper function in the laser diode driver, but the current depends on which laser is being hooked up.
Using a mock instrument might allow one to apply some sort of Model
to the laser diode + laser diode driver system. Here is what it might look like in code.
ldd = load_instrument('MyLaserDiodeDriver')
lucent = load_mock_instrument('MyLucentLaser')
lucent.current = ldd.current
lucent.temperature = ldd.temperature
def power_model(self):
threshold = Q_('10 mA')
power = (self.current - threshold) * Q_('1 mW/10 mA').to('mW')
return power
from types import MethodType # in order to bind the function so we can use self
lucent.power = MethodType(power_model, lucent)
# then
ldd.set_current = Q_('20 mA')
print(lucent.power)
# 1 mW
Eventually, it'd be nice to write code so that lucent
could influence ldd
. Perhaps through a setter
and getter
method.
I'll write up a longer response with some suggestions when I get a chance, but first another question: are you expecting to use this for mocking a "real" instrument (for testing higher-level code, etc.), creating a "manual" instrument for devices that cannot be computer controlled, or for both purposes?
There's probably pros and cons to combining vs a different class for "manual" and "mock" instruments. Combining means less complexity, less testing, less documentation, and I think if we're clever, we can write a broad enough class to handle both. At this point, people seem to have been using Instrumental for 5+ years without this feature, so I think for now, having one class should be fine.
Let's start with the idea of a "manual" instrument, since that seems more fundamental to me. A "mock" instrument could then be a manual instrument that's defined (manually or perhaps automatically) to mimic the interface of an existing "real" instrument.
At the moment, I don't see an immediate benefit to an inheritable ManualInstrument
class (but I'm open to the idea). Instead, you could just define an ordinary Instrument
that has ManualFacet
s, e.g.
class MyPowerSupply(Instrument):
voltage = ManualFacet(units='volts')
current = ManualFacet(units='amps')
>>> ps = MyPowerSupply()
>>> ps.voltage = '12 V'
>>> ps.voltage
<Quantity(12, 'volt')>
ManualFacet
should be easy to create by inheriting from Facet
and automatically supplying an fset
and fget
that simply store and retrieve the value from the instrument instance.
I imagine you may want to create small manual Instrument
subclasses that aren't intended for inclusion in Instrumental's set of drivers. I imagine you would simply have a local module/package that defines these classes outside of the Instrumental package. I think all of this should work ok.
The next step would be to consider whether it's useful or worth it to allow registering of (or auto-register) these Instrument
subclasses so that they can be opened by instrument()
. Probably not a priority, but worth thinking about. This integration might allow you to save instruments as well, so they're easily visible/accessible through list_instrument()
.
I created a new branch manual-inst and added a prototype ManualFacet
and added some changes to allow use of Instrument
classes provided by external packages. I've only tested it a bit, and I'm sure there are some things that will have to be cleaned up (as far as external-package drivers), but it lets you do the following:
from instrumental.drivers import Instrument, ManualFacet
class MyPowerSupply(Instrument):
_INST_PARAMS_ = ['name']
voltage = ManualFacet(units='volts')
current = ManualFacet(units='amps')
def _initialize(self):
self.name = self._paramset['name']
You can then use the instrument as you'd expect:
>>> ps = MyPowerSupply('myps')
>>> ps.voltage
<Quantity(0, 'volt')>
>>> ps.voltage = '12 V'
>>> ps.voltage
<Quantity(12, 'volt')>
>>> ps.save_instrument('my_power_supply')
>>> ps = instrument('my_power_supply')
>>> ps.current
<Quantity(0, 'ampere')>
When you load a saved instrument, it tries to import the module in which its Instrument
subclass was defined, so this should not change. Therefore the module with your class definition should either be on the PYTHONPATH (perhaps in site-packages) or in the working directory where you intend to use it.
Take a look and let me know what you think.
Thanks! I just got back from a vacation. This looks great!
I'm going to work on writing some models this week that implement this code, and will report back some comments here!
Finally looking at this now.
Am I supposed to manually register this new driver with instrumental somehow. I’m getting this error:
---------------------------------------------------------------------------Exception
Traceback (most recent call
last){}
not
".format(name) +--> 985 "found in config
file") 986 987 params = ParamSet(**raw_params) # Copy
first to avoid modifying input dicts
Exception: Instrument with alias foo
not found in config file
On July 27, 2018 at 11:52:06 AM, Nate Bogdanowicz ([email protected]) wrote:
I created a new branch manual-inst https://github.com/mabuchilab/Instrumental/tree/manual-inst and added a prototype ManualFacet and added some changes to allow use of Instrument classes provided by external packages. I've only tested it a bit, and I'm sure there are some things that will have to be cleaned up (as far as external-package drivers), but it lets you do the following:
from instrumental.drivers import Instrument, ManualFacet
class MyPowerSupply(Instrument): INST_PARAMS = ['name'] voltage = ManualFacet(units='volts') current = ManualFacet(units='amps')
def _initialize(self):
self.name = self._paramset['name']
You can then use the instrument as you'd expect:
ps = MyPowerSupply('myps') ps.voltage <Quantity(0, 'volt')> ps.voltage = '12 V' ps.voltage <Quantity(12, 'volt')> ps.save_instrument('my_power_supply')
ps = instrument('my_power_supply') ps.current <Quantity(0, 'ampere')>
When you load a saved instrument, it tries to import the module in which its Instrument subclass was defined, so this should not change. Therefore the module with your class definition should either be on the PYTHONPATH (perhaps in site-packages) or in the working directory where you intend to use it.
Take a look and let me know what you think.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/mabuchilab/Instrumental/issues/56#issuecomment-408508240, or mute the thread https://github.com/notifications/unsubscribe-auth/AHIjaFweAOA1GSzj8ULNgpG0tS8_n-yyks5uK2FVgaJpZM4VR_Yd .
I'm traveling today, but I'll give it a look when I get a chance.
In the meantime, make sure you're using the manual-inst
branch and that your driver's module is importable as described above.
I think the problem is that I didn’t figure out how to save the driver in the right place. I’ll have to tinker with this a little more. I was merely trying to define the driver int he same script as execution
I got it working with the external package. Two things I'm working on now:
- I wasn't able to get the state to persist when saving the file.
- After opening a saved file, I'm unsure how to save it a second time, or whether that is intuitively what we want to do anyway?
I'm not exactly sure what your most recent comment means. By persistent state, do you mean e.g. the voltage values? That's not currently something that's supported, but I'm open to it. And what is the "file" you're talking about saving. Are you talking about a saved instrument? At this point, saved instruments are merely a set of parameters that are used to locate and open a device (obviously, this was originally built for real hardware).
Yes, I was imagining that when I set the value of a ManualFacet, that the value would be written through to the disk, and available every time it’s loaded.
Yes, I meant “saved instrument” by file.
I see now. That's not something that's implemented yet, but it certainly could be.
I think it's probably best to keep this out of instrumental.conf
(where the saved instruments are stored), but could either be a single file (JSON maybe) with settings for all mock instruments, or one file per instrument. I'm leaning towards the latter, as it prevents separate instruments in separate processes from trying to write to the same file.
The first challenge is probably to decide how best to uniquely identify each mock instrument. We could use "name" (alias) and class/module name. They could be used as a unique pair, or we could just use the alias and optionally verify that it was created with the same class that we're trying to use to open it with now. Or the check could be more "structural" or duck-typed, where it need only have (a subset of) fields that match the class's.
I agree that since these manual instruments are categorically different from everything else that instrumental does, for instrumental to treat them differently. I think the biggest difference is that most of the conventional instruments that instrumental interfaces keep track of their own state.
python
>>> from instrumetal import instrument
>>> ps = instrument('power_supply_with_remote_interface')
>>> ps.voltage = '12 V'
>>> exit()
python
>>> from instrumetal import instrument
>>> ps = instrument('power_supply_with_remote_interface')
>>> ps.voltage
Quantity(12, 'Volts')
Thus, even when python closes, the state is preserved across sessions.
I would advocate for pickling the manual instruments. There may be situations where I may want to save, for example, an IV curve in a manual instrument as an additional property of the of the class. JSON would probably not be able to capture this.
Definitely agree with one file per instrument, for the reasons that you described, and also portability of individual files.
Yes, pickling could work well, especially if we require the instrument's class/module to be importable. It's worth considering whether we should use an older pickle protocol to allow these files to be read across python versions.
It looks like the current implementation has no notion of a manual/mock instrument, but merely manual facets. So, perhaps we save the status of any ManualFacet
under it's instrument's alias?
A major question here is how we integrate this with the notion of an instrument's "alias". As I mentioned above, passing an alias to instrument()
simply checks the instrumental.conf
file for an entry matching this alias, then uses these params to look for and open an instrument. This means that there can be multiple different aliases used for a single instrument, perhaps with different settings attached to them. I think the current implementation will attach this alias to the Instrument
instance once it's opened (or when it's saved). If the instrument is not opened via an alias, it has no alias.
So, it may make sense to restrict saving of ManualFacet
s to instances that have an alias. We might add two different hooks to save the data:
- Save an instrument's facets just before it is closed, or just as it goes out of scope or the interpreter is closing.
- Add an option to
ManualFacet
to save every time its value is set.
I know that older versions of pickle cannot handle lambda functions, etc.... I would have to monkey around with some more use cases to figure out if we could handle everything via Python2's pickle, but right now I think the answer is yes.
Is there a need to mix ManualFacets with the conventional facets? I.e. can an instrument be part manual, and part conventional? My first guess is no, in which case we could just worry about manual/mock instruments all together, without having to worry about instrumental.conf
at all...
I think it's possible but unlikely (or uncommon) that an instrument could have some remote control, with a few things on the front panel that can only be switched manually. I can't think of any such specific device at the moment, but it seems possible.
If we were to make manual/mock and conventional instruments mutually exclusive, how would you propose implementing the saving feature?
We could have a separate directory (perhaps determined by some conf/env variable) where all the pickled objects could go.
I just pushed some changes that add save-state functionality. Some notes:
- Instruments can save and load their state, provided they have an
alias
(which they will have when saved viasave_instrument()
or loaded by alias). -
ManualFacet
s have a new setting to enable autosaving of their instrument's state upon setting the facet's value. - I'm pickling the instrument's
__dict__
instead of the instrument itself due to complications in the instrument creation process (caused b/c we reimplement__new__()
). This means you don't unpickle the instrument directly, but instead open it via alias, then load its state. We could add a setting to enable automatic loading of this state, if desired.
If you modify the MyPowerSupply
implementation to pass save_on_set=True
for the ManualFacet
s, you should now be able to do something like this:
>>> ps = MyPowerSupply('whatever')
>>> ps.save_instrument('myps')
>>> ps.current = '500 mA' # _save_state() auto-called if save_on_set is True
Close the interpreter and reopen,
>>> ps = instrument('myps')
>>> ps.current
<Quantity(0, 'ampere')>
>>> ps._load_state()
>>> ps.current
<Quantity(0.5, 'ampere')>
Note that the power supply's name
parameter has nothing to do with its alias. A real implementation probably shouldn't have such a name
parameter.
Some additional things we might consider doing:
- [ ] Add general setting to auto-load saved instrument state
- [ ] Add auto-saving before
close()
is called - [ ] Expose load/save as public methods?
- [ ] Add conf setting for the saved-state directory
- [ ] Determine what the default saved-state directory should be called
Let me know what you think