-
Notifications
You must be signed in to change notification settings - Fork 301
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
Conversation
Codecov Report
@@ 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 |
Any news for this PR? @WilliamHPNielsen, @jenshnielsen |
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 |
Thank you for the feedback @jenshnielsen , I'll try to write some tests. BTW, what are the plans for the long term? |
Not very concrete at the moment but we want to design some more generic objects that can abstract multiple instruments and how they interact |
@jenshnielsen tests added |
There was a problem hiding this 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.
qcodes/instrument_drivers/devices.py
Outdated
is_divider = division is not None | ||
is_amplifier = gain is not None | ||
|
||
if not (is_divider ^ is_amplifier): |
There was a problem hiding this comment.
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’? :)
qcodes/instrument_drivers/devices.py
Outdated
DIVISION = enum.auto() | ||
|
||
|
||
class ParameterScaler(Parameter): |
There was a problem hiding this comment.
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!
qcodes/instrument_drivers/devices.py
Outdated
instrument=getattr(self._wrapper_param, "_instrument", None), | ||
label=self.label, | ||
unit=self.unit, | ||
metadata=self._wrapper_param.metadata) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
qcodes/instrument_drivers/devices.py
Outdated
name: str=None, | ||
label: str=None, | ||
unit: str=None) -> None: | ||
self._wrapper_param = output |
There was a problem hiding this comment.
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.
qcodes/instrument_drivers/devices.py
Outdated
self.label = name | ||
else: | ||
if self.role == Role.DIVISION: | ||
self.label = "{}_attenuated".format(self._wrapper_param.label) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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=
ordivision=
) - 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)
qcodes/instrument_drivers/devices.py
Outdated
|
||
@gain.setter | ||
def gain(self, gain: Union[int, float, Parameter]): | ||
self.role = Role.GAIN |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
qcodes/instrument_drivers/devices.py
Outdated
self._save_val(value) | ||
return value | ||
|
||
def get_instrument_value(self) -> Union[int, float]: |
There was a problem hiding this comment.
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”.
qcodes/instrument_drivers/devices.py
Outdated
|
||
def set_raw(self, value: Union[int, float]) -> None: | ||
""" | ||
Se the value on the instrument, accounting for the scaling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo : “set”
There was a problem hiding this comment.
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”
qcodes/instrument_drivers/devices.py
Outdated
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
qcodes/tests/test_parameter.py
Outdated
@@ -904,6 +905,57 @@ def tearDown(self): | |||
del self.d | |||
|
|||
|
|||
class TestParameterScaler(TestCase): | |||
def setUp(self): |
There was a problem hiding this comment.
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?
@astafan8 Ok, I made the modifications that you suggested, except for the factory and role |
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? |
indeed @YakBizzarro , very good remark, let's put this class into |
docs/examples/Paramter Scaler.ipynb
Outdated
@@ -0,0 +1,252 @@ | |||
{ |
There was a problem hiding this comment.
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
qcodes/instrument_drivers/devices.py
Outdated
is_amplifier = gain is not None | ||
|
||
if not xor(is_divider, is_amplifier): | ||
raise ValueError('Provide only division OR gain') |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
qcodes/instrument/parameter.py
Outdated
@@ -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): |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, seems legit
qcodes/instrument/parameter.py
Outdated
self._meta_attrs.extend(["role"]) | ||
self.metadata['wrapped_parameter'] = self._wrapped_parameter.name | ||
try: | ||
self.metadata['wrapped_instrument'] = self._wrapped_instrument.name |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
Is it good now or should I do some other modification? |
@YakBizzarro yes, there is a discussion going on here about whether the "role" part is needed at all. could you take a look? |
@astafan8 ops, completely missed it, thank you |
@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! |
@YakBizzarro Please fix the |
@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 |
@YakBizzarro Well, they are not false positives :) the And the " 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. |
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