ParameterNode guide

The ParameterNode is a container that can group parameters, and also other ParameterNodes.
For those familiar with object-oriented programming, the ParameterNode can be viewed as the object, and the Parameters as the attributes.

One distinction with is that ParameterNodes can be nested, and so another way to look at a ParameterNode is as a dictionary, where its items can either be other dictionaries (ParameterNodes), or values (Parameters). It is therefore recommended to have a basic understanding of Python dictionaries, see e.g. https://www.w3schools.com/python/python_dictionaries.asp

One example of a ParameterNode is an Instrument, which corresponds to a physical instrument.
However, the ParameterNode does not need to be limited to actual instruments.

ParameterNode with Parameters

Here we create a ParameterNode and add a Parameter to it.
Note that we do not need to specify the parameter name, it is automatically deduced from the attribute name:
[ ]:
from qcodes import ParameterNode, Parameter

node = ParameterNode('node')
node.p = Parameter(set_cmd=None, initial_value=1)  # Can also use node.add_parameter('p', set_cmd=None)
print('node.p.name =', node.p.name)
node.p.name = p

The Parameter is registered in the ParameterNode’s attribute parameters:

[ ]:
node.parameters
{'p': <qcodes.instrument.parameter.Parameter: p at 1929329519528>}

Similarly, ParameterNode is added as the parent of the Parameter:

[ ]:
node.p.parent
ParameterNode node containing 1 parameters

This is also reflected when we get a string representation of the parameter:

[ ]:
str(node.p)
'node_p'

Note: Once a Parameter is added to a ParameterNode, adding the Parameter to other ParameterNodes does not change it’s parent

ParameterNode containing ParameterNodes

A ParameterNode can also contain other ParameterNodes:

[ ]:
node.subnode = ParameterNode()

node.subnode.p = Parameter(set_cmd=None)

str(node.subnode.p)
'node_subnode_p'

Simplified snapshotting

When performing a measurement, a snapshot is created of all Parameters and ParameterNodes in the station.
These snapshots can become quite messy:
[ ]:
node.snapshot()
{'__class__': 'qcodes.instrument.parameter_node.ParameterNode',
 'functions': {},
 'name': 'node',
 'parameter_nodes': {'subnode': {'__class__': 'qcodes.instrument.parameter_node.ParameterNode',
   'functions': {},
   'name': 'subnode',
   'parameter_nodes': {},
   'parameters': {'p': {'__class__': 'qcodes.instrument.parameter.Parameter',
     'full_name': 'node_subnode_p',
     'label': 'P',
     'name': 'p',
     'raw_value': None,
     'ts': None,
     'value': None}},
   'submodules': {}}},
 'parameters': {'p': {'__class__': 'qcodes.instrument.parameter.Parameter',
   'full_name': 'node_p',
   'label': 'P',
   'name': 'p',
   'raw_value': 1,
   'ts': '2018-11-22 17:23:15',
   'value': 1}},
 'submodules': {}}
Depending on the type of ParameterNode, we may only be interested in the values of the parameters and nodes.
For this reason, we can improve the readability of the snapshot by turning on simplify_snapshot:
[ ]:
node.simplify_snapshot = True
node.subnode.simplify_snapshot = True
[ ]:
node.snapshot()
{'__class__': 'qcodes.instrument.parameter_node.ParameterNode',
 'p': 1,
 'subnode': {'__class__': 'qcodes.instrument.parameter_node.ParameterNode',
  'p': None}}

We can also print a snapshot of all parameters and parameter nodes:

[ ]:
node.print_snapshot()
node :
        parameter value
--------------------------------------------------------------------------------
p :     1

node_subnode :
        parameter value
--------------------------------------------------------------------------------
p :     None

Accessing Parameters as attributes

One may have noticed that getting/setting a parameter is different from standard python classes.
For parameters, this is done by function calls:
[ ]:
node = ParameterNode()
node.p = Parameter(set_cmd=None)
node.p(42)  # Setting value
node.p()  # Getting value
42

Compare this to a standard python class, where the attribute behaves just like the variable it represents

[ ]:
class C:
    pass

c = C()
c.p = 1  # Setting value
c.p  # Getting value
1
This can be confusing, especially if we want to convert abstract classes into ParameterNodes and Parameters.
We would need to remember which classes are and are not ParameterNodes, and which attributes are and are not Parameters.
As a solution to this problem, we can pass the keyword argument use_as_atttributes to a ParameterNode:
[ ]:
node = ParameterNode(use_as_attributes=True)
node.p = Parameter(set_cmd=None)
node.p = 42  # Setting value
node.p  # Getting value
42
We see that now the Parameter attributes behave just like the variables they represent.
There are cases, however, where you still want to access the Parameter object instead of its value.
In this case, you can access the Parameter like you would in a dictionary:
[ ]:
print(repr(node['p']))
node['p'].snapshot()
<qcodes.instrument.parameter.Parameter: p at 1929470616296>
{'__class__': 'qcodes.instrument.parameter.Parameter',
 'full_name': 'p',
 'label': 'P',
 'name': 'p',
 'raw_value': 42,
 'ts': '2018-11-22 17:23:18',
 'value': 42}
Warning: The option use_as_attributes can have unintended consequences, as every call to the attribute will trigger it’s get function.
This could cause unnecessary get calls if this is not taken into account:
[ ]:
node = ParameterNode(use_as_attributes=True)
node.p = Parameter(get_cmd=lambda: print('Expensive get command called'))

for k in range(5):
    print(node.p)
Expensive get command called
None
Expensive get command called
None
Expensive get command called
None
Expensive get command called
None
Expensive get command called
None
As a general rule, all Instruments should have use_as_attributes=False to ensure that we don’t accidentally perform actions on instruments that may negatively impact the experiment.
For more abstract ParameterNodes, especially ones where parameters don’t perform ancillary get/set functions, using use_as_attributes=True is preferred.

Defining Parameter properties via the ParameterNode

In more advanced cases, a ParameterNode contains parameters whose get/set need While in principle these functions can be passed

In more advanced cases, the Parameter may not be isolated, but instead depends on other Parameters in its ParameterNode.
In these cases, every time

As an example, lets say we have a Pulse class with a start time t_start and stop time t_stop:

[ ]:
class Pulse(ParameterNode):
    def __init__(self, t_start, t_stop, **kwargs):
        super().__init__(use_as_attributes=True, **kwargs)
        self.t_start = Parameter(set_cmd=None, initial_value=t_start)
        self.t_stop = Parameter(set_cmd=None, initial_value=t_stop)

    def __repr__(self):
        return f'Pulse(t_start={self.t_start}, t_stop={self.t_stop})'

Pulse(1, 3)
Pulse(t_start=1, t_stop=3)
So far so good. Now we also want this Pulse to have a duration, defined by duration = t_stop - t_start.
When we set the duration, we want t_start to remain fixed, and t_stop should be changed accordingly.
This adds a complication, as the parameter duration depends on two other parameters.

A basic solution would be also instantiate the duration parameter with get_cmd and set_cmd, as such:

self.duration = Parameter(get_cmd=lambda: self.t_stop - self.t_start,
                          set_cmd=lambda duration: setattr(self, 't_stop', self.t_start + duration))

While this in principle works, it fails for functions that are not one-liners, plus using lambda functions can have unintended consequences. Other solutions are to either define the get/set functions elsewhere, or to subclass the Parameter and explicitly defining a get_raw and set_raw method. Both these options are not ideal, one of the reasons being that it obfuscates code.

As an alternative, we can define the get/set of the Parameter as methods in the ParameterNode:

[ ]:
from qcodes.instrument.parameter_node import parameter
class Pulse(ParameterNode):
    def __init__(self, t_start, t_stop, **kwargs):
        super().__init__(use_as_attributes=True, **kwargs)
        self.t_start = Parameter(set_cmd=None, initial_value=t_start)
        self.t_stop = Parameter(set_cmd=None, initial_value=t_stop)
        self.duration = Parameter()

    @parameter
    def duration_get(self, parameter):
        return self.t_stop - self.t_start

    @parameter
    def duration_set(self, parameter, duration):
        self.t_stop = self.t_start + duration

    def __repr__(self):
        return f'Pulse(t_start={self.t_start}, t_stop={self.t_stop}, duration={self.duration})'

pulse = Pulse(1, 3)
pulse
Pulse(t_start=1, t_stop=3, duration=2)
[ ]:
pulse.duration = 5
pulse
Pulse(t_start=1, t_stop=6, duration=5)
We see that in this case the duration parameter can easily access other parameters in the parameter node.
We use the decorator @parameter to define that the method belongs to a Parameter, and the method should have the form {parameter_name}_{function}, where {function} can be get/set/get_parser/set_parser/vals