Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paramterScaler with an usage example and deprecate VoltageDivider #1069

Merged
merged 14 commits into from
Jul 30, 2018

Conversation

YakBizzarro
Copy link
Contributor

@YakBizzarro YakBizzarro commented Apr 23, 2018

Here I propose an enhanced version of the VoltageDivider. Compared to it, it has the ability to set either a division or a gain, depending on the nature of the instrument attached to it. Unit of the resulting parameter can be changed. Moreover, the gain/division can be a Qcodes parameter, in the case that is set by a remote instrument and not locally.
We have been using this object in SpinQubit group in QuTech for a few months with modules in IVVI/SPI racks, so it's well tested on the field.

@WilliamHPNielsen, @jenshnielsen

@codecov
Copy link

codecov bot commented Apr 23, 2018

Codecov Report

Merging #1069 into master will increase coverage by 0.2%.
The diff coverage is 94.04%.

@@            Coverage Diff            @@
##           master    #1069     +/-   ##
=========================================
+ Coverage   79.96%   80.16%   +0.2%     
=========================================
  Files          49       49             
  Lines        6673     6762     +89     
=========================================
+ Hits         5336     5421     +85     
- Misses       1337     1341      +4

@YakBizzarro
Copy link
Contributor Author

Any news for this PR? @WilliamHPNielsen, @jenshnielsen

@jenshnielsen
Copy link
Collaborator

Sorry for the delay @YakBizzarro I am not absolutely sure this it the way we want to do this in the long term but for now it should be fine. I would prefer not to deprecate the voltage divider just yet. And I would very much like to see this have some tests if possible

@YakBizzarro
Copy link
Contributor Author

Thank you for the feedback @jenshnielsen , I'll try to write some tests. BTW, what are the plans for the long term?

@jenshnielsen
Copy link
Collaborator

Not very concrete at the moment but we want to design some more generic objects that can abstract multiple instruments and how they interact

@YakBizzarro
Copy link
Contributor Author

@jenshnielsen tests added

Copy link
Contributor

@astafan8 astafan8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for implementing this! But please fix some software-design related things: the cleaner the implementation, the more stable and useful the feature.

is_divider = division is not None
is_amplifier = gain is not None

if not (is_divider ^ is_amplifier):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For readability reasons, coups you use ‘xor’ from ‘from operator import xor’? :)

DIVISION = enum.auto()


class ParameterScaler(Parameter):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition to qcodes! Thank you!

instrument=getattr(self._wrapper_param, "_instrument", None),
label=self.label,
unit=self.unit,
metadata=self._wrapper_param.metadata)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the result of implementing metadata inclusion like this? Are the attributes of the ‘_wrapper_parameter’ going to be included into the snapshot next to the attributes of the scaler, or in a separate field? I prefer the latter case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the metadata inclusion from the parent. As long as the parent instrument is noted in the metadata, there is no need to copy its metadata

name: str=None,
label: str=None,
unit: str=None) -> None:
self._wrapper_param = output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please rename ‘_wrapper_parameter’ to ‘_wrapped_parameter’? The reason is that scaler wraps the output, hence the output is wrapped inside the scaler, and the scaler is the wrapper here.

self.label = name
else:
if self.role == Role.DIVISION:
self.label = "{}_attenuated".format(self._wrapper_param.label)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The amount of times decisions are made based on the ‘role’ means that this should be implemented via factory so that the code is more maintainable. Is it clear what I am suggesting, or do you need help/support with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can't really understand this. Do you have some example/doc?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) There is a design pattern called "factory", it allows you to create similar objects while keeping the specifics of the objects inside them. For example, now the ParameterScaler can be a divider, an amplifier, and also knows how to choose its role. The class becomes too big, complex, with repetitive code (the if role == .. stuff), not easily testable as you have to remember about all the possible things the class can do. Instead, an more maintainable design is to have a Divider class, an Amplifier class, and a "factory" class that provides the interface for creating them.

You can read more on the wiki or take a look at a somewhat close example in Python.

So my proposal would be to:

  • define Divider class (perhaps private so that it cannot be directly instantiated) that only divides
  • define Amplifier class (perhaps private so that it cannot be directly instantiated) that only amplifies
  • keep the ParameterScaler class as an "interface" that users will use to create Dividers or Amplifiers based on the key-value argument (gain= or division=)
  • refactor existing tests so that they correspond to these classes.

@YakBizzarro , in case you don't have time or you find it troublesome to implement the factory, would you allow me to do it? (this will mean that I will push some commits to you branch)


@gain.setter
def gain(self, gain: Union[int, float, Parameter]):
self.role = Role.GAIN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see any use in changing the role after the ScalerParameter object is created? I personally don’t because it may lead to errors in users code (also when the role is changed, labels and names are not updated) - thus, I’d suggest that if the role is “gain”, it should not be possible to set the “division” and vice versa.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the contrary, I found very useful sometimes when changing the gain, to change also the way how's expressed (division or gain). Also some instruments have a variable gain that can be either less or more then one. I would rather remove the _attenuated/_amplified naming and use only "_scaled"

self._save_val(value)
return value

def get_instrument_value(self) -> Union[int, float]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it called instrument value? What is returned is the value of the wrapped parameter, so I expect that the name is smth like “get_unscaled_value” or better “get_wrapped_parameter_value”.


def set_raw(self, value: Union[int, float]) -> None:
"""
Se the value on the instrument, accounting for the scaling
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo : “set”

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: not “instrument” but “wrapped parameter” or “parameter that is being scaled”

instrument_value = value * self._multiplier()

# disable type check due to https://github.com/python/mypy/issues/2128
instrument_value = cast(Union[int, float], instrument_value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this needed for? Also, instead of union(int,float) could we use the “type” information from the wrapped parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized that the comment is wrong. This line was inserted in 43a4dd0 by @jenshnielsen , do you have any comment? I'm not very used to python typing system

@@ -904,6 +905,57 @@ def tearDown(self):
del self.d


class TestParameterScaler(TestCase):
def setUp(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add tests for name, label, and unit logic? Also test for metadata working correctly? Also for the fact that an exception is thrown if one is creating a scaler parameter and supplies both gain and decision to the constructor?

@YakBizzarro
Copy link
Contributor Author

@astafan8 Ok, I made the modifications that you suggested, except for the factory and role

@YakBizzarro
Copy link
Contributor Author

Btw, initially I put this class in qcodes/instruments_drivers/devices.py because I was extending VoltageDivider, but since now it's a very generic helper, would it better to have it in qcodes/instrument/parameter.py?

@astafan8
Copy link
Contributor

astafan8 commented Jul 4, 2018

indeed @YakBizzarro , very good remark, let's put this class into qcodes/instrument/parameter.py

@@ -0,0 +1,252 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/examples/Paramter Scaler.ipynb -> docs/examples/Parameter Scaler.ipynb

is_amplifier = gain is not None

if not xor(is_divider, is_amplifier):
raise ValueError('Provide only division OR gain')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not allow both? Have both default to 1 instead of None, drop self.Role, in the constructor set self._multiplier = gain / division, and have get_raw return self.wrapped_parameter() * self._multiplier()? I'm probably missing something, but that seems simpler in both usage and implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see an use case for having both a multiplier and a divider. Currently they are just defined as the mutual reciprocal (divider = 1/gain or gain = 1/divider) as user convenience. For example, for a a resistive divider, you can say that is divided by 1000 or that it has a gain of 0.001, and it's totally equivalent

Copy link
Contributor

@spxtr spxtr Jul 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the root of my confusion is that it doesn't seem like the Role enum is necessary at all. If they call division then just divide_multiplier by the given factor.

You have this:

    class Role(enum.Enum):
        GAIN = enum.auto()
        DIVISION = enum.auto()


    def __init__(self,
                 output: Parameter,
                 division: Union[int, float, Parameter] = None,
                 gain: Union[int, float, Parameter] = None,
                 name: str=None,
                 label: str=None,
                 unit: str=None) -> None:

        # (...)

        # Set the role, either as divider or amplifier
        # Raise an error if nothing is specified
        is_divider = division is not None
        is_amplifier = gain is not None

        if not xor(is_divider, is_amplifier):
            raise ValueError('Provide only division OR gain')

        if is_divider:
            self.role = ParameterScaler.Role.DIVISION
            self._multiplier = division
        elif is_amplifier:
            self.role = ParameterScaler.Role.GAIN
            self._multiplier = gain

        # (...)

    # Division of the scaler
    @property
    def division(self):
        if self.role == ParameterScaler.Role.DIVISION:
            return self._multiplier()
        elif self.role == ParameterScaler.Role.GAIN:
            return 1 / self._multiplier()

    @division.setter
    def division(self, division: Union[int, float, Parameter]):
        self.role = ParameterScaler.Role.DIVISION
        self._multiplier = division

    # Getter and setter for the real value
    def get_raw(self) -> Union[int, float]:
        """
        Returns:
            number: value at which was set at the sample
        """
        if self.role == ParameterScaler.Role.GAIN:
            value = self._wrapped_parameter() * self._multiplier()
        elif self.role == ParameterScaler.Role.DIVISION:
            value = self._wrapped_parameter() / self._multiplier()

        self._save_val(value)
        return value

Could you do this instead? To me at least it is much simpler.

    def __init__(self,
                 output: Parameter,
                 division: Union[int, float, Parameter] = 1,
                 gain: Union[int, float, Parameter] = 1,
                 name: str=None,
                 label: str=None,
                 unit: str=None) -> None:

        # (...)

        self._multiplier = gain / division

        # (...)

    # Division of the scaler
    @property
    def division(self):
        return 1/self._multiplier()

    @division.setter
    def division(self, division: Union[int, float, Parameter]):
        self._multiplier = 1/division

    # Getter and setter for the real value
    def get_raw(self) -> Union[int, float]:
        """
        Returns:
            number: value at which was set at the sample
        """
        value = self._wrapped_parameter() * self._multiplier()

        self._save_val(value)
        return value

Not a big deal though, feel free to ignore :)

Copy link
Contributor

@astafan8 astafan8 Jul 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of @spxtr that the Role is not needed, but the self._multiplier = gain / division part seems confusing. I'd suggest that one can initialize a ParameterScaler via gain OR division kwarg (as it is now), but instead of having a Role, either gain or division are converted to multiplier. I mean smth like this:

    def __init__(self,
                 output: Parameter,
                 division: Union[int, float, Parameter] = 1,
                 gain: Union[int, float, Parameter] = 1,
                 name: str=None,
                 label: str=None,
                 unit: str=None) -> None:

        # (...)

        
        if not xor(gain is None, division is None):
            raise KeyError('Either gain or division should be supplied')

        if gain:  # gain is defined as scaled_value/wrapped_value
            self.gain = gain  # magic happens in the property setter
        if division:  # division is defined as 1/(scaled_value/wrapped_value)
            self.division = division  # magic happens in the property setter

        # (...)

    # Division of the scaler
    @property
    def division(self):
        return 1/self._multiplier()

    @division.setter
    def division(self, division: Union[int, float, Parameter]):
        self._multiplier.set(1/division)

    # Gain of the scaler
    @property
    def gain(self):
        return self._multiplier()

    @gain.setter
    def gain(self, gain: Union[int, float, Parameter]):
        self._multiplier.set(gain)

    # Getter for the real value
    def get_raw(self) -> Union[int, float]:
        """
        Returns:
            number: value at which was set at the sample
        """
        value = self._wrapped_parameter() * self._multiplier()

        self._save_val(value)
        return value

This eliminates the need for the role (testing is easier, no "factory" requests from me :) ).

@YakBizzarro what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Role is needed in the case when the multiplier is not a simple scalar, but a Qcodes parameter (look at the last example). In this case 1/division should be computed every time that we set a value, or (like a do), perform a multiplication, depending on the role value

Copy link
Contributor

@astafan8 astafan8 Jul 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I get it: if the multiplier is a qcodes parameter, then the ScaledParameter needs to know what the role of the multiplier is in order to perform the right operation (multiplication or division)...

ok, the number of responsibilities/blows/whistles of the ScaledParameter is exploding.

@YakBizzarro Could you list the use cases that this class is intended to support? Is the following list complete ? :

  • create a new parameter that has a fixed and unchangeable scaling factor (both <1 or >1) for another parameter
  • create a new parameter that multiplies another parameter with a third parameter
  • create a new parameter that divides another parameter by a third parameter
    If so, then I see at least 3 different classes (and possibly a couple of extra private ones to "modularize" the implementation).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Just in the first case, the scaling factor can be changed manually by the user using the setter of division/gain. Useful for IVVI modules with manual gain/division

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter cases are intended for instruments where the gain is selectable by the computer or when is function of an external value

@@ -1545,3 +1547,197 @@ def __init__(self, name, instrument=None, initial_value=None, **kwargs):
super().__init__(name=name, instrument=instrument,
get_cmd=None, set_cmd=None,
initial_value=initial_value, **kwargs)


class ParameterScaler(Parameter):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YakBizzarro , sorry for the late comment, but I would like to rename this class to ScaledParameter. The reason is the following. A "scaler" (so far as I my knowledge of English is concerned) is smth that "scales" smth. According to the implementation, the internal "multiplier" parameter is doing the actual "scaling", while the "ParameterScaler" instance just returns the "scaled" value of the wrapped parameter. In other words, if I call get for the "ParameterScaler" I get the "scaled" value. Thus, "ParameterScaler" shall be called "ScaledParameter".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, seems legit

self._meta_attrs.extend(["role"])
self.metadata['wrapped_parameter'] = self._wrapped_parameter.name
try:
self.metadata['wrapped_instrument'] = self._wrapped_instrument.name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of try-catch please use getattr function with an "if" statement:

if self._wrapped_instrument:
    self.metadata['wrapped_instrument'] = getattr(self._wrapped_instrument, "name", None)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@YakBizzarro
Copy link
Contributor Author

Is it good now or should I do some other modification?

@astafan8
Copy link
Contributor

astafan8 commented Jul 6, 2018

@YakBizzarro yes, there is a discussion going on here about whether the "role" part is needed at all. could you take a look?

@YakBizzarro
Copy link
Contributor Author

@astafan8 ops, completely missed it, thank you

@astafan8
Copy link
Contributor

astafan8 commented Jul 26, 2018

@YakBizzarro After some time thinking about it and also discussing with the core team, my conclusion is that I am going to merge this PR "as is". I appreciate your patience, and effort spent in fixing comments and adding tests!

@astafan8
Copy link
Contributor

@YakBizzarro Please fix the mypy errors (see Travis output) and codacy complaints. And then I'll merge.

@YakBizzarro
Copy link
Contributor Author

@astafan8 Thank you! I fixed the mypy errors; most of the codacy issues are false positives (it complains that methods are not callable, which is true, but the values returned by this methods are callable), but I don't get why it complain about overloading get_raw/set_raw

@astafan8
Copy link
Contributor

@YakBizzarro Well, they are not false positives :)

the get_raw/set_raw one comes from a confusion with Parameter and _BaseParameter classes - in the first, you have to redefine get/set methods, while in the second you have to redefine get_raw/set_raw; but because constructor of Parameter calls the constructor of the _BaseParameter before dealing with get/set methods, you can redefine get_raw/set_raw even though you are inheriting from Parameter. But this is not your fault, we will clean up Parameter and _BaseParameter classes in some time in the future.

And the "_multiplier is not callable" can be solved by first getting it into a local variable like multiplier_param = self._multiplier, and then calling the new variable. Extra type annotations can help with making this code understandable.

But as discussed before, this implementation has quite some other more important problems, so for the sake of time, let's not bother about these minor issues now. I am going to merge.

@astafan8 astafan8 merged commit 82cb20e into microsoft:master Jul 30, 2018
giulioungaretti pushed a commit that referenced this pull request Jul 30, 2018
Merge: e9e6f1f 723d55a
Author: Mikhail Astafev <astafan8@gmail.com>

    Merge pull request #1069 from YakBizzarro/paramterscaler
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants