4. Subsystems and channels

Complex instruments usually have too many capabilities to fit reasonably in a single namespace, which is why SCPI commands usually define a hierarchy. Furthermore, either because the instrument is made of multiple parts or because the notion is built-in the instrument, another recurrent notion is the one of channel. Channels are usually identified by an id and share their capabilities. To handle those two cases I3py uses the notions of “subsystems” and “channels”. As channels inherit a number of capabilities from subsystems, we will first describe them before moving on to the specificities of channels.

4.1. Subsystems

Subsystems act mainly as container and provide little capabilities by themselves. They do however allow to group options and checks for all their features and actions. The next following two sections focus on their declaration and on the working principle of options and checks.

4.1.1. Declaration

Subsystems can be declared in the body of a driver using the following syntax as already mentioned in Writing a driver.

class MyDriver(VisaMessageDriver):
    """My driver with a subsystem.

    """

    oscillator = subsystem()
    with oscillator as o:

        o.frequency = Float('OSC:FREQ?', 'OSC:FREQ {}')

        @o
        @Action()
        def is_sync(self):
            pass

Once created, the use of a context manager allows for the use of short names but also some additional magic and it should hence be used.

While convenient, this syntax can be cumbersome if one needs to declare nested subsystems/channels and as presented here would lead to large amount of code duplication for similar instruments. To allow the declaration of subsystems outside of a driver declaration, subsystem supports to be passed a list of base classes as first argument (‘bases’). Those base classes do not have to be subsystems themselves (subclass of AbstractSubSystem) and if none of them are, a proper class will be added by the framework.

When subclassing a driver which has subsystems, one can modify the subsystems (adding/modifying actions/features) by simply redeclaring it with the same name and proceeding as for a new one. The framework will identify that the subsystem already exists and will use the version present on the base class as base class for the subsystem.

Note

In the case of multiple inheritance, if several of the driver base classes declare the same subsystem, the framework will use the one present on the first class of the mro (Method Resolution Order, ie the leftmost in the class creation). Other classes can be added as arguments of subsystem.

4.1.2. Options and checks

As mentioned in the introduction, subsystems can define tests (options and checks) that apply to all their features and actions. Those can be declared just like for features and actions by passing strings defining the options (‘options’ argument) and checks (‘checks’ argument).

The options will be tested when one try to access the subsystem from the driver:

ss = driver.subsystem

If the tests do not evaluate to true, an AttributeError will be raised mimicking a missing attribute. And as for all other options test the result will be cached. To implement this, the subsystem is accessed through a descriptor.

Note

By default, the framework uses i3py.core.subsystem.SubSystemDescriptor as descriptor to protect the access to a subsystem. You can specify an alternative descriptor using the ‘descriptor’ argument of subsystem. Alternative descriptor should inherit from i3py.core.abstract.AbstractSubSystemDescriptor.

Checks on the other hand are run each time a feature is accessed or an action is run. To achieve this, the framework customize the features/actions of the subsystem by adding an ‘enabling’ step to pre_get/set/call.

Note

When a inheriting a subsystem from a parent driver, the options and checks defined in the subsystem call are appended to the ones existing on the subsystem of the parent driver.

4.1.3. Features working in subsystems

In order for features to work in subsystems, subsystems implement: default_get_feature(), default_set_feature(), default_check_operation(). As a subsystem is nothing but a container, it simply propagate the call to its parent, without altering the arguments.

4.2. Channels

In several respects, channels are very similar to subsystems. Just as them, they follow mostly the same logic as far as subclassing is concerned and also support checks and options which work in the same way. The key difference between subsystems and channels is that where only one subsystem is instantiated per driver, multiple instances of a channel can be tied to the same driver. The following section will describe the differences between channels and subsystems.

4.2.1. Declaration

Channels are declared in the body of a driver using the following syntax as already hinted in Writing a driver. The key difference with a subsystem is that a way to identify the valid channels id is generally required as first argument.

class MyDriver(VisaMessageDriver):
    """My driver with a channel.

    """

    channels = channel((1, 2, 3),
                       aliases={1: ('A', 'a'), 2: 'B', 3: 'C'})
    with channels as c:

        c.frequency = Float('CH{ch_id}:FREQ?', 'OSC:FREQ {}')

        @c
        @Action()
        def is_sync(self):
            pass

The valid ids for channel can be declared as above as a tuple or list, which make sense when the number of channel is hardcoded in the device. Alternatively, one can pass the name of a method existing on the parent whose signature should be (self) -> Iterable.

In some cases, it may be handy to provide alternate names for channels for the sake of clarity. One can do so by declaring aliases. Aliases should be a dictionary whose keys match the ids of the channels and whose values are the allowed alternatives. Alternatives can be specified either as a simple value or as a list/tuple.

When subclassing a driver which has channels, if no channels ids are provided the method used on the parent driver will be inherited, and the aliases mapping will be updated with any new value provided (note that this will use the provided dict to update the inherited one such that duplicate keys will be overridden).

Note

As for subsystems one can specify base classes for a channel and the same inheritance rules apply.

4.2.2. Usage

As explained in the user guide, channel instances can be accessed using the following syntax:

driver.channels[ch_id]

where ch_id would 1, 2, 3 or any of their aliases in the previous case.

To achieve this and allow to check for options too, the channel machinery uses, like subsystems, a descriptor to protect the access to the object storing the channel instances, which we will refer to as the channel container. To make things clear, when writing:

c = driver.channels

c is the channel container returned by the descriptor. In addition to supporting subscription, the container is iterable and has the following attributes:

  • available: list of the ids of the channels that can be accessed.
  • aliases: mapping between the declared aliases and the matching channel id.

By default, the framework will use i3py.core.base_channel.ChannelDescriptor for the descriptor and ChannelContainer for the channel container. Just like for subsystems, it is possible to substitute to those classes custom ones using the descriptor_type and the container_type keyword arguments. The substitution classes should inherit from the proper abstract classes: i3py.core.abstract.AbstractChannelDescriptor and AbstractChannelContainer respectively.

4.2.3. Features working in channels

In order for features to work in channels, channels implement: default_get_feature(), default_set_feature(), default_check_operation(). In the case of the first two methods, a channel add its id under the keyword argument ch_id to the keyword arguments and propagate the call the parent driver. For the third method the call is simply forwarded on the parent.

The default behaviour is well fitted for VISA message based instruments when the channel id is part of the command as in this case things work out the box. The user simply has to indicate where to format the channel id, as illustrated in the above example. For instrument that requires first the channel to be selected, it is simply a matter of overriding the method to prepend the channel selection command.