mabuchilab / Instrumental

Python-based instrumentation library from the Mabuchi Lab.
http://instrumental-lib.readthedocs.org/
GNU General Public License v3.0
117 stars 77 forks source link

An instrument with two "states" #128

Closed NitramC closed 3 years ago

NitramC commented 3 years ago

I am having a hard time deciding how to deal with a lock-in amplifier (Signal Recovery 7265) that effectively can be in one of two states, depending on the value of a parameter "REFMODE". image image

So in the single reference state I would like to have class with this one property:

class SR7265(Lockin, VisaMixin):

    mag = SCPI_Facet(
        'MAG.',
        ...
    )

and in a dual reference state I would like to have a class with these two properties:

class SR7265(Lockin, VisaMixin):

    mag1 = SCPI_Facet(
        'MAG1.',
        ...
    )

    mag2 = SCPI_Facet(
        'MAG2.',
        ...
    )

I have thought about dynamically removing and adding properties on a change of REFMODE, but it does not look like you can do that at an instance level -- you would have to do it at a class level, which would not be acceptable with multiple instruments.

I guess the most straight forward approach is to just have all three properties in the class, but it seems a shame to clutter the namespace with properties that don't really exist.

Any ideas?

natezb commented 3 years ago

Yeah, it is a bit ugly, but having all three facets is probably the best solution. Even if you could find a way to dynamically add and remove them, it's likely to be error-prone and lead to user confusion. For instance, imagine the state gets changed out-of band, or you're writing code in an IDE with code completion and can't access a property that would be available at that point of the code.

natezb commented 3 years ago

More generally, if you're interested in thinking about more ergonomic ways to interact with "channels" in instruments, you can check out the scopes/tektronix driver, where I was playing around with the idea of Channel and Channels objects.

You could imagine having some kind of "Channel"-like object that can either address both channels simultaneously, or be indexed to address them individually. For example, applied to magnitudes:

# "dual mode"
inst.mag = x

# "single mode"
inst.mag[1] = x
inst.mag[2] = y

This also might be a terrible and confusing idea, but it's fun to think about.

NitramC commented 3 years ago

The last idea looks pretty promising to me. I will take a look at the scopes module and give it some more thought. Thanks!

NitramC commented 3 years ago

OK, so this should have the behaviour you suggested:

class RefModeDependentFacet(object):
    def __init__(self, doc=None, facetnames=[None]*3):
        self.__doc__ = doc
        self.facetnames = facetnames

    def __get__(self, obj, objtype):
        if obj.ref_mode == 0:
            return getattr(obj, self.facetnames[0])
        else:
            return self.DualModeContainer(obj, self.facetnames)

    def __set__(self, obj, value):
        if obj.ref_mode == 0:
            setattr(obj, self.facetnames[0], value)
        else:
            return self.DualModeContainer(obj, self.facetnames)

    class DualModeContainer():
        def __init__(self, obj, facetnames):
            self.obj = obj
            self.facetnames = facetnames

        def __getitem__(self, key):
            if key == 1:
                return getattr(self.obj, self.facetnames[1])
            elif key == 2:
                return getattr(self.obj, self.facetnames[2])
            else:
                raise KeyError('Invalid key')

        def __setitem__(self, key, value):
            if key == 1:
                return setattr(self.obj, self.facetnames[1], value)
            elif key == 2:
                return setattr(self.obj, self.facetnames[2], value)
            else:
                raise KeyError('Invalid key')
class SR7265(Lockin, VisaMixin):

    mag = RefModeDependentFacet(
        facetnames=['_mag', '_mag1', '_mag2'])

    _mag = SCPI_Facet(
        'MAG.',
        ...
    )

    _mag1 = SCPI_Facet(
        'MAG1.',
        ...
    )

    _mag2 = SCPI_Facet(
        'MAG2.',
        ...
    )

In the end though, I think what I am trying to do is purely esthetic. The only benefit of this approach is that it hides three private facets behind one public one in the attribute list when tab-completing. The user will still need to be aware of the current REFMODE, and how to properly interact with this new Facet wrapper. There is also the added overhead of having to check the REFMODE on each get/set of a RefModeDependentFacet. One could cache the REFMODE value, but then there is the issue of out-of-band changes.

I am going to stick with the ugly straight-forward approach (that is also easier to maintain).