1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
|
.. _channels:
Instruments with channels
=========================
.. testsetup::
# Behind the scene, replace Instrument with FakeInstrument to enable
# doctesting simple usage cases (default doctest group)
from pymeasure.instruments.fakes import FakeInstrument as Instrument
.. testsetup:: with-protocol-tests
# If we want to run protocol tests on doctest code, we need to use a
# separate doctest "group" and a different set of imports.
# See https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html
from pymeasure.instruments import Instrument, Channel
from pymeasure.test import expected_protocol
Some instruments, like oscilloscopes and voltage sources, have channels whose commands differ only in the channel name.
For this case, we have :class:`~pymeasure.instruments.Channel`, which is similar to :class:`~pymeasure.instruments.Instrument` and its property factories, but does expect an :class:`~pymeasure.instruments.Instrument` instance (i.e., a parent instrument) instead of an :class:`~pymeasure.adapters.Adapter` as parameter.
All the channel communication is routed through the instrument's methods (`write`, `read`, etc.).
However, :meth:`Channel.insert_id <pymeasure.instruments.Channel.insert_id>` uses ``str.format`` to insert the channel's id at any occurrence of the class attribute :attr:`Channel.placeholder`, which defaults to :code:`"ch"`, in the written commands.
For example :code:`"Ch{ch}:VOLT?"` will be sent as :code:`"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
.. code::
.. 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 :code:`: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, :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` should be used
to explicitly declare each individual channel. For instruments with more than 16 channels, the
:class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` can create multiple channels in a single declaration.
Adding a channel with :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator`
*******************************************************************************************
For instruments with fewer than 16 channels the class :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` should be used to assign each channel interface to a class attribute.
:class:`~pymeasure.instruments.common_base.CommonBase.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 :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator`, ``output_A`` and ``output_B``, which will become our channel interfaces.
.. testcode:: with-protocol-tests
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")
.. testcode:: with-protocol-tests
:hide:
with expected_protocol(ExtremeVoltage5000,
[("SOURceA:VOLT 1.25", None), ("SOURceB:VOLT?", "4.56")],
name="Instrument with Channels",
) as inst:
inst.output_A.voltage = 1.25
assert inst.channels['B'].voltage == 4.56
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 :code:`channels`.
We can access the channel interface through that class name:
.. code-block:: python
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
.. testcode:: with-protocol-tests
:hide:
with expected_protocol(ExtremeVoltage5000,
[("SOURceA:VOLT 50", None), ("SOURceB:VOLT?", "4.56")],
name="Instrument with Channels",
) as inst:
inst.output_A.voltage = 50
assert inst.output_B.voltage == 4.56
Or we can access the channel interfaces through the :code:`channels` collection:
.. code-block:: python
# Set channel A voltage
extreme_inst.channels['A'].voltage = 50
# Read channel B voltage
chan_b_voltage = extreme_inst.channels['B'].voltage
.. testcode:: with-protocol-tests
:hide:
with expected_protocol(ExtremeVoltage5000,
[("SOURceA:VOLT 50", None), ("SOURceB:VOLT?", "4.56")],
name="Instrument with Channels",
) as inst:
inst.channels['A'].voltage = 50
assert inst.channels['B'].voltage == 4.56
Adding multiple channels with :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator`
********************************************************************************************************
For instruments greater than 16 channels the class :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` can be used to easily generate a list of channels from one class attribute declaration.
The :class:`~pymeasure.instruments.common_base.CommonBase.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 :code:`"ch_"`) and channel id, e.g. the channel with id "A" will be added as attribute :code:`ch_A`.
While :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` creates a channel interface for each class attribute, :class:`~pymeasure.instruments.common_base.CommonBase.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 :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` to generate 24 channels of the ``VoltageChannel`` class.
.. testcode:: with-protocol-tests
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)))
.. testcode:: with-protocol-tests
:hide:
with expected_protocol(MultiExtremeVoltage5000,
[("SOURce5:VOLT 1.23", None), ("SOURce16:VOLT?", "4.56")],
name="Instrument with Channels",
) as inst:
inst.ch_5.voltage = 1.23
assert inst.channels[16].voltage == 4.56
We can now access the channel interfaces through the generated class attributes:
.. code-block:: python
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
.. testcode:: with-protocol-tests
:hide:
with expected_protocol(MultiExtremeVoltage5000,
[("SOURce5:VOLT 50", None), ("SOURce16:VOLT?", "4.56")],
name="Instrument with Channels",
) as inst:
inst.ch_5.voltage = 50
assert inst.ch_16.voltage == 4.56
Because we use `channels` as the class attribute for ``MultiChannelCreator``, we can access the channel interfaces through the :code:`channels` collection:
.. code-block:: python
# Set channel 10 voltage
extreme_inst.channels[10].voltage = 50
# Read channel 22 voltage
chan_b_voltage = extreme_inst.channels[22].voltage
.. testcode:: with-protocol-tests
:hide:
with expected_protocol(MultiExtremeVoltage5000,
[("SOURce10:VOLT 50", None), ("SOURce22:VOLT?", "4.56")],
name="Instrument with Channels",
) as inst:
inst.channels[10].voltage = 50
assert inst.channels[22].voltage == 4.56
Advanced channel management
***************************
Adding / removing channels
--------------------------
In order to add or remove programmatically channels, use the parent's :meth:`~pymeasure.instruments.common_base.CommonBase.add_child`, :meth:`~pymeasure.instruments.common_base.CommonBase.remove_child` methods.
Channels with fixed prefix
--------------------------
If all channel communication is prefixed by a specific command, e.g. :code:`"SOURceA:"` for channel A, you can override the channel's :meth:`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.
.. testcode:: with-protocol-tests
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.""",
)
.. testcode:: with-protocol-tests
:hide:
class InstrumentWithChannelsPrefix(Instrument):
"""An instrument with a channel, just for the test."""
ch_A = Instrument.ChannelCreator(VoltageChannelPrefix, "A")
with expected_protocol(InstrumentWithChannelsPrefix,
[("SOURceA:VOLT 1.23", None), ("SOURceA:VOLT?", "1.23")],
name="Test",
) as inst:
inst.ch_A.voltage = 1.23
assert inst.ch_A.voltage == 1.23
This channel class implements the same communication as the previous example, but implements the channel prefix in the :meth:`insert_id` method and not in the individual property (created by :meth:`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.
.. testcode:: with-protocol-tests
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)
.. testcode:: with-protocol-tests
:hide:
with expected_protocol(MultiChannelTypeInstrument,
[("SOURceB:VOLT 1.23", None), ("SOURce2:VOLT?", "4.56")],
name="MultiChannelTypeInstrument",
) as inst:
inst.an_B.voltage = 1.23
assert inst.di_2.voltage == 4.56
This instrument has two collections of channels and one single channel.
The first collection in the dictionary :code:`analog` contains an instance of :class:`VoltageChannel` with the name :code:`an_A` and an instance of :class:`VoltageChannelPrefix` with the name :code:`an_B`.
The second collection contains three channels of type :class:`VoltageChannel` with the names :code:`di_0, di_1, di_2` in the dictionary :code:`digital`.
You can address the first channel of the second group either with :code:`inst.di_0` or with :code:`inst.digital[0]`.
Finally, the instrument has a single channel with the name :code:`power`, as it does not have a prefix.
If you have a single channel category, do not change the default parameters of :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` or :meth:`~pymeasure.instruments.common_base.CommonBase.add_child`, in order to keep the code base homogeneous.
We expect the default behaviour to be sufficient for most use cases.
|