Source code for instruments.thorlabs.pm100usb
#!/usr/bin/env python
"""
Provides the support for the Thorlabs PM100USB power meter.
"""
# IMPORTS #####################################################################
import logging
from collections import defaultdict, namedtuple
from enum import Enum, IntEnum
from instruments.units import ureg as u
from instruments.generic_scpi import SCPIInstrument
from instruments.util_fns import enum_property
# LOGGING #####################################################################
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
# CLASSES #####################################################################
[docs]
class PM100USB(SCPIInstrument):
"""
Instrument class for the `ThorLabs PM100USB`_ power meter.
Note that as this is an SCPI-compliant instrument, the properties and
methods of :class:`~instruments.generic_scpi.SCPIInstrument` may be used
as well.
.. _ThorLabs PM100USB: http://www.thorlabs.com/thorproduct.cfm?partnumber=PM100USB
"""
# ENUMS #
[docs]
class SensorFlags(IntEnum):
"""
Enum containing valid sensor flags for the PM100USB
"""
is_power_sensor = 1
is_energy_sensor = 2
response_settable = 16
wavelength_settable = 32
tau_settable = 64
has_temperature_sensor = 256
[docs]
class MeasurementConfiguration(Enum):
"""
Enum containing valid measurement modes for the PM100USB
"""
current = "CURR"
power = "POW"
voltage = "VOLT"
energy = "ENER"
frequency = "FREQ"
power_density = "PDEN"
energy_density = "EDEN"
resistance = "RES"
temperature = "TEMP"
# We will cheat and also represent things by a named tuple over bools.
# TODO: make a flagtuple into a new type in util_fns, copying this out
# as a starting point.
_SensorFlags = namedtuple(
"SensorFlags",
[flag.name for flag in SensorFlags], # pylint: disable=not-an-iterable
)
# INNER CLASSES #
[docs]
class Sensor:
"""
Class representing a sensor on the ThorLabs PM100USB
.. warning:: This class should NOT be manually created by the user. It
is designed to be initialized by the `PM100USB` class.
"""
def __init__(self, parent):
self._parent = parent
# Pull details about the sensor from SYST:SENSOR:IDN?
sensor_idn = parent.query("SYST:SENSOR:IDN?")
(
self._name,
self._serial_number,
self._calibration_message,
self._sensor_type,
self._sensor_subtype,
self._flags,
) = sensor_idn.split(",")
# Normalize things to enums as appropriate.
# We want flags to be a named tuple over bools.
# pylint: disable=protected-access
self._flags = parent._SensorFlags(
**{
e.name: bool(e & int(self._flags))
for e in PM100USB.SensorFlags # pylint: disable=not-an-iterable
}
)
@property
def name(self):
"""
Gets the name associated with the sensor channel
:type: `str`
"""
return self._name
@property
def serial_number(self):
"""
Gets the serial number of the sensor channel
:type: `str`
"""
return self._serial_number
@property
def calibration_message(self):
"""
Gets the calibration message of the sensor channel
:type: `str`
"""
return self._calibration_message
@property
def type(self):
"""
Gets the sensor type of the sensor channel
:type: `str`
"""
return self._sensor_type, self._sensor_subtype
@property
def flags(self):
"""
Gets any sensor flags set on the sensor channel
:type: `collections.namedtuple`
"""
return self._flags
# PRIVATE ATTRIBUTES #
_cache_units = False
# UNIT CACHING #
@property
def cache_units(self):
"""
If enabled, then units are not checked every time a measurement is
made, reducing by half the number of round-trips to the device.
.. warning::
Setting this to `True` may cause incorrect values to be returned,
if any commands are sent to the device either by its local panel,
or by software other than InstrumentKit.
:type: `bool`
"""
return bool(self._cache_units)
@cache_units.setter
def cache_units(self, newval):
self._cache_units = (
self._READ_UNITS[self.measurement_configuration] if newval else False
)
# SENSOR PROPERTIES #
@property
def sensor(self):
"""
Returns information about the currently connected sensor.
:type: :class:`PM100USB.Sensor`
"""
return self.Sensor(self)
# SENSING CONFIGURATION PROPERTIES #
# TODO: make a setting of this refresh cache_units.
measurement_configuration = enum_property(
"CONF",
MeasurementConfiguration,
doc="""
Returns the current measurement configuration.
:rtype: :class:`PM100USB.MeasurementConfiguration`
""",
)
@property
def averaging_count(self):
"""
Integer specifying how many samples to collect and average over for
each measurement, with each sample taking approximately 3 ms.
"""
return int(self.query("SENS:AVER:COUN?"))
@averaging_count.setter
def averaging_count(self, newval):
if newval < 1:
raise ValueError("Must count at least one time.")
self.sendcmd(f"SENS:AVER:COUN {newval}")
# METHODS ##
_READ_UNITS = defaultdict(lambda: u.dimensionless)
_READ_UNITS.update(
{
MeasurementConfiguration.power: u.W,
MeasurementConfiguration.current: u.A,
MeasurementConfiguration.frequency: u.Hz,
MeasurementConfiguration.voltage: u.V,
}
)
[docs]
def read(self, size=-1, encoding="utf-8"):
"""
Reads a measurement from this instrument, according to its current
configuration mode.
:param int size: Number of bytes to read from the instrument. Default
of ``-1`` reads until a termination character is found.
:units: As specified by :attr:`~PM100USB.measurement_configuration`.
:rtype: :class:`~pint.Quantity`
"""
# Get the current configuration to find out the units we need to
# attach.
units = (
self._READ_UNITS[self.measurement_configuration]
if not self._cache_units
else self._cache_units
)
return float(self.query("READ?", size)) * units