Coding Style¶
Data Types¶
Numeric Data¶
When appropriate, use pint.Quantity
objects to track units.
If this is not possible or appropriate, use a bare float
for scalars
and np.ndarray
for array-valued data.
Boolean and Enumerated Data¶
If a property or method argument can take exactly two values,
of which one can be interpreted in the affirmative, use Python
bool
data types to represent this. Be permissive in what you accept
as True
and False
, in order to be consistent with Python conventions
for truthy and falsey values. This can be accomplished using the
bool
function to convert to Booleans, and is done implicitly by
the if
statement.
If a property has more than two permissible values, or the two allowable
values are not naturally interpreted as a Boolean (e.g.: positive/negative,
AC/DC coupling, etc.), then consider using an Enum
or IntEnum
as
provided by enum
. The latter is useful in for wrapping integer values that
are meaningful to the device.
For example, if an instrument can operate in AC or DC mode, use an enumeration like the following:
class SomeInstrument(Instrument):
# Define as an inner class.
class Mode(Enum):
"""
When appropriate, document the enumeration itself...
"""
#: ...and each of the enumeration values.
ac = "AC"
#: The "#:" notation means that this line documents
#: the following member, SomeInstrument.Mode.dc.
dc = "DC"
# For SCPI-like instruments, enum_property
# works well to expose the enumeration.
# This will generate commands like "MODE AC"
# and "MODE DC".
mode = enum_property(
name=":MODE",
enum=SomeInstrument.Mode,
doc="""
And here is the docstring for this property
"""
)
# To set the mode is now straightforward.
ins = SomeInstrument.open_somehow()
ins.mode = ins.Mode.ac
Note that the enumeration is an inner class, as described below in Associated Types.
Object Oriented Design¶
Associated Types¶
Many instrument classes have associated types, such as channels and axes, so that these properties of the instrument can be manipulated independently of the underlying instrument:
>>> channels = [ins1.channel[0], ins2.channel[3]]
Here, the user of channels
need not know or care that the two
channels are from different instruments, as is useful for large
installations. This lets users quickly redefine their setups
with minimal code changes.
To enable this, the associated types should be made inner classes
that are exposed using ProxyList
.
For example:
class SomeInstrument(Instrument):
# If there's a more appropriate base class, please use it
# in preference to object!
class Channel:
# We use a three-argument initializer,
# to remember which instrument this channel belongs to,
# as well as its index or label on that instrument.
# This will be useful in sending commands, and in exposing
# via ProxyList.
def __init__(self, parent, idx):
self._parent = parent
self._idx = idx
# define some things here...
@property
def channel(self):
return ProxyList(self, SomeInstrument.Channel, range(2))
This defines an instrument with two channels, having labels 0
and 1
.
By using an inner class, the channel is clearly associated with the instrument,
and appears with the instrument in documentation.
Since this convention is somewhat recent, you may find older code that uses a style more like this:
class _SomeInstrumentChannel:
# stuff
class SomeInstrument(Instrument):
@property
def channel(self):
return ProxyList(self, _SomeInstrumentChannel, range(2))
This can be redefined in a backwards-compatible way by bringing the channel class inside, then defining a new module-level variable for the old name:
class SomeInstrument(Instrument):
class Channel:
# stuff
@property
def channel(self):
return ProxyList(self, _SomeInstrumentChannel, range(2))
_SomeInstrumentChannel = SomeInstrument.Channel