Instruments with channels

Some instruments, like oscilloscopes and voltage sources, have channels whose commands differ only in the channel name. For this case, we have Channel, which is similar to Instrument and its property factories, but does expect an Instrument instance (i.e., a parent instrument) instead of an Adapter as parameter. All the channel communication is routed through the instrument’s methods (write, read, etc.). However, Channel.insert_id uses str.format to insert the channel’s id at any occurrence of the class attribute Channel.placeholder, which defaults to "ch", in the written commands. For example "Ch{ch}:VOLT?" will be sent as "Ch3:VOLT?" to the device, if the channel’s id is “3”.

Please add any created channel classes to the documentation. In the instrument’s documentation file, you may add

.. autoclass:: pymeasure.instruments.MANUFACTURER.INSTRUMENT.CHANNEL
    :members:
    :show-inheritance:

MANUFACTURER is the folder name of the manufacturer and INSTRUMENT the file name of the instrument definition, which contains the CHANNEL class. You may link in the instrument’s docstring to the channel with :class:`CHANNEL`

To simplify and standardize the creation of channels in an Instrument class, there are two classes that can be used. For instruments with fewer than 16 channels, ChannelCreator should be used to explicitly declare each individual channel. For instruments with more than 16 channels, the MultiChannelCreator can create multiple channels in a single declaration.

Adding a channel with ChannelCreator

For instruments with fewer than 16 channels the class ChannelCreator should be used to assign each channel interface to a class attribute. ChannelCreator constructor accepts two parameters, the channel class for this channel interface, and the instrument’s channel id for the channel interface.

In this example, we are defining a channel class and an instrument driver class. The VoltageChannel channel class will be used for controlling two channels in our ExtremeVoltage5000 instrument. In the ExtremeVoltage5000 class we declare two class attributes with ChannelCreator, output_A and output_B, which will become our channel interfaces.

class VoltageChannel(Channel):
    """A channel of the voltage source."""

    voltage = Channel.control(
        "SOURce{ch}:VOLT?", "SOURce{ch}:VOLT %g",
        """Control the output voltage of this channel.""",
    )

class ExtremeVoltage5000(Instrument):
    """An instrument with channels."""
    output_A = Instrument.ChannelCreator(VoltageChannel, "A")
    output_B = Instrument.ChannelCreator(VoltageChannel, "B")

At instrument class instantiation, the instrument class will create an instance of the channel class and assign it to the class attribute name. Additionally the channels will be collected in a dictionary, by default named channels. We can access the channel interface through that class name:

extreme_inst = ExtremeVoltage5000('COM3')
# Set channel A voltage
extreme_inst.output_A.voltage = 50
# Read channel B voltage
chan_b_voltage = extreme_inst.output_B.voltage

Or we can access the channel interfaces through the channels collection:

# Set channel A voltage
extreme_inst.channels['A'].voltage = 50
# Read channel B voltage
chan_b_voltage = extreme_inst.channels['B'].voltage

Adding multiple channels with MultiChannelCreator

For instruments greater than 16 channels the class MultiChannelCreator can be used to easily generate a list of channels from one class attribute declaration.

The MultiChannelCreator constructor accepts a single channel class or list of channel classes, and a list of corresponding channel ids. Instead of lists, you may also use tuples. If you give a single class and a list of ids, all channels will be of the same class.

At instrument instantiation, the instrument will generate channel interfaces as class attribute names composing of the prefix (default "ch_") and channel id, e.g. the channel with id “A” will be added as attribute ch_A. While ChannelCreator creates a channel interface for each class attribute, MultiChannelCreator creates a channel collection for the assigned class attribute. It is recommended you use the class attribute name channels to keep the codebase homogenous.

To modify our example, we will use MultiChannelCreator to generate 24 channels of the VoltageChannel class.

class VoltageChannel(Channel):
    """A channel of the voltage source."""

    voltage = Channel.control(
        "SOURce{ch}:VOLT?", "SOURce{ch}:VOLT %g",
        """Control the output voltage of this channel.""",
    )

class MultiExtremeVoltage5000(Instrument):
    """An instrument with channels."""
    channels = Instrument.MultiChannelCreator(VoltageChannel, list(range(1,25)))

We can now access the channel interfaces through the generated class attributes:

extreme_inst = MultiExtremeVoltage5000('COM3')
# Set channel 5 voltage
extreme_inst.ch_5.voltage = 50
# Read channel 16 voltage
chan_16_voltage = extreme_inst.ch_16.voltage

Because we use channels as the class attribute for MultiChannelCreator, we can access the channel interfaces through the channels collection:

# Set channel 10 voltage
extreme_inst.channels[10].voltage = 50
# Read channel 22 voltage
chan_b_voltage = extreme_inst.channels[22].voltage

Advanced channel management

Adding / removing channels

In order to add or remove programmatically channels, use the parent’s add_child(), remove_child() methods.

Channels with fixed prefix

If all channel communication is prefixed by a specific command, e.g. "SOURceA:" for channel A, you can override the channel’s insert_id() method. That is especially useful, if you have only one channel of that type, e.g. because it defines one function of the instrument vs. another one.

class VoltageChannelPrefix(Channel):
    """A channel of a voltage source, every command has the same prefix."""

    def insert_id(self, command):
        return f"SOURce{self.id}:{command}"

    voltage = Channel.control(
        "VOLT?", "VOLT %g",
        """Control the output voltage of this channel.""",
    )

This channel class implements the same communication as the previous example, but implements the channel prefix in the insert_id() method and not in the individual property (created by control()).

Collections of different channel types

Some devices have different types of channels. In this case, you can specify a different collection and prefix parameter.

class PowerChannel(Channel):
    """A channel controlling the power."""
    power = Channel.measurement(
        "POWER?", """Measure the currently consumed power.""")

class MultiChannelTypeInstrument(Instrument):
    """An instrument with two different channel types."""
    analog = Instrument.MultiChannelCreator(
        (VoltageChannel, VoltageChannelPrefix),
        ("A", "B"),
        prefix="an_")
    digital = Instrument.MultiChannelCreator(VoltageChannel, (0, 1, 2), prefix="di_")
    power = Instrument.ChannelCreator(PowerChannel)

This instrument has two collections of channels and one single channel. The first collection in the dictionary analog contains an instance of VoltageChannel with the name an_A and an instance of VoltageChannelPrefix with the name an_B. The second collection contains three channels of type VoltageChannel with the names di_0, di_1, di_2 in the dictionary digital. You can address the first channel of the second group either with inst.di_0 or with inst.digital[0]. Finally, the instrument has a single channel with the name power, as it does not have a prefix.

If you have a single channel category, do not change the default parameters of ChannelCreator or add_child(), in order to keep the code base homogeneous. We expect the default behaviour to be sufficient for most use cases.