"""Device driver for Thorlabs PM100 series optical power and energy meters."""
import os
from pathlib import Path
from herosdevices.core import DeviceCommandQuantity
from herosdevices.core.templates import VisaDeviceTemplate
from herosdevices.helper import explicit, mark_driver
from herosdevices.interfaces.atomiq import Measurable
[docs]
def usbtmc_address_to_resource(address: str) -> str:
"""Convert a Linux usbtmc device address into a VISA resource string.
If the given device node does not exist, the kernel usbtmc driver is probably not bound to the device
anymore: userspace VISA access (pyvisa-py) detaches the kernel driver and it is only re-attached when the
device is reconnected. In this case the USB bus is scanned for USBTMC devices instead. If exactly one is
found, it is used, otherwise an error is raised.
Args:
address: Path of the usbtmc device, e.g. ``/dev/usbtmc0``.
Returns:
The VISA resource string of the device.
"""
device = Path(address).stat()
sysfs_path = Path(f"/sys/dev/char/{os.major(device.st_rdev)}:{os.minor(device.st_rdev)}")
# the char device entry points to <usb device>/<usb interface>/usbmisc/usbtmcN
try:
usb_device_path = next(parent for parent in sysfs_path.resolve().parents if (parent / "idVendor").exists())
except StopIteration:
msg = f"Could not resolve the sysfs entry of the usbtmc device {address} to a USB device"
raise FileNotFoundError(msg) from None
vendor = (usb_device_path / "idVendor").read_text().strip()
product = (usb_device_path / "idProduct").read_text().strip()
serial = (usb_device_path / "serial").read_text().strip()
resource = f"USB::0x{vendor}::0x{product}"
if serial:
resource += f"::{serial}"
return f"{resource}::INSTR"
[docs]
@mark_driver(
info="Optical Power Meter",
product_page="https://www.thorlabs.com/thorproduct.cfm?partnumber=PM100D",
additional_docs=["/tutorials/thorlabs_pm100.rst"],
state="beta",
)
class PM100(VisaDeviceTemplate, Measurable):
"""Device driver for the Thorlabs PM100A/D optical power and energy meter.
The device is controlled via SCPI commands over a VISA connection (typically USBTMC).
Args:
resource: Resource of the device. Either a Linux usbtmc device (e.g. ``/dev/usbtmc0``), which is
automatically resolved to the corresponding VISA resource string, or a VISA resource string
directly (e.g. ``USB::0x1313::0x8078::P0019289::INSTR``).
keep_alive: Keep the VISA connection open between operations.
Warning:
Note, that if a device file (``/dev/usbtmc0``) is used, the device can not be restarted and needs to be
reconnected to the host due to the PyVISA backend removing the ``/dev/usbtmc0`` link upon access. You need to
use a VISA string (printed on startup of the device) to be able to restart it for example via BOSS.
"""
measure_target: str = "power"
"""Set what the :py:meth:`PM100.measure` method returns. Can be "power", "voltage" or "frequency" """
power: float = DeviceCommandQuantity(command_get="MEAS:POW?", dtype=float, unit="W", poll_interval_limit=0.1)
"""Optical power in W (or dBm if :code:`power_unit` is set to "DBM")."""
voltage: float = DeviceCommandQuantity(command_get="MEAS:VOLT?", dtype=float, unit="V", poll_interval_limit=0.1)
"""Voltage in V."""
wavelength: float = DeviceCommandQuantity(
command_set="SENS:CORR:WAV {}", command_get="SENS:CORR:WAV?", dtype=float, unit="nm"
)
"""Operation wavelength for the wavelength correction in nm. Only settable if the sensor supports it."""
frequency: float = DeviceCommandQuantity(command_get="MEAS:FREQ?", dtype=float, unit="Hz", poll_interval_limit=0.1)
"""Pulse repetition rate in Hz."""
beam_diameter: float = DeviceCommandQuantity(
command_set="SENS:CORR:BEAM {}", command_get="SENS:CORR:BEAM?", dtype=float, unit="mm"
)
"""Beam diameter in mm, used for power/energy density calculations."""
attenuation: float = DeviceCommandQuantity(
command_set="SENS:CORR:LOSS {}", command_get="SENS:CORR:LOSS?", dtype=float, unit="dB"
)
"""User attenuation (positive) / gain (negative) factor in dB."""
averaging_rate: int = DeviceCommandQuantity(
command_set="SENS:AVER:COUN {}", command_get="SENS:AVER:COUN?", dtype=int
)
"""Averaging rate, one sample takes approximately 3 ms (3000 averages take approximately 1 s)."""
power_unit: str = DeviceCommandQuantity(
command_set="SENS:POW:UNIT {}", command_get="SENS:POW:UNIT?", dtype=str, value_check_fun=explicit(["W", "DBM"])
)
"""Unit for power readings, either "W" or "DBM"."""
power_range: float = DeviceCommandQuantity(
command_set="SENS:POW:RANG:UPP {}", command_get="SENS:POW:RANG:UPP?", dtype=float, unit="W"
)
"""Upper limit of the power measurement range in W."""
voltage_range: float = DeviceCommandQuantity(
command_set="SENS:VOLT:RANG:UPP {}", command_get="SENS:VOLT:RANG:UPP?", dtype=float, unit="V"
)
"""Upper limit of the voltage measurement range in V."""
power_autorange: int = DeviceCommandQuantity(
command_set="SENS:POW:RANG:AUTO {}",
command_get="SENS:POW:RANG:AUTO?",
dtype=int,
value_check_fun=explicit([0, 1]),
)
"""Auto-ranging of the power measurement (0: off, 1: on)."""
voltage_autorange: int = DeviceCommandQuantity(
command_set="SENS:VOLT:RANG:AUTO {}",
command_get="SENS:VOLT:RANG:AUTO?",
dtype=int,
value_check_fun=explicit([0, 1]),
)
"""Auto-ranging of the voltage measurement (0: off, 1: on)."""
sensor_type: int = DeviceCommandQuantity(
command_get="SYST:SENS:IDN?", dtype=str, format_fun=lambda x: x.split(",")[0]
)
"""Type of the connected sensor as reported by the device."""
def __init__(self, resource: str, keep_alive: bool = True, **kwargs) -> None:
if "::" not in resource:
resource = usbtmc_address_to_resource(resource)
VisaDeviceTemplate.__init__(
self, resource, keep_alive=keep_alive, read_termination="\n", write_termination="\n", **kwargs
)
Measurable.__init__(self)
[docs]
def zero_adjust(self) -> None:
"""Run the zero adjustment (dark offset) routine.
Make sure the sensor is blocked from all light of the beam source during this routine, otherwise all
subsequent measurements will have an offset.
"""
self.connection.write("SENS:CORR:COLL:ZERO:INIT")
[docs]
def measure(self) -> float:
"""Perform a measurement depending on the setting of :py:attr:`PM100.measure_target`.
Returns:
The measured quantity in SI base units ("W", "V" or "Hz")
"""
if self.measure_target == "power":
return self.power
if self.measure_target == "voltage":
return self.voltage
if self.measure_target == "frequency":
return self.frequency
msg = f"Unknown measure_target: {self.measure_target}, must be 'power', 'voltage' or 'frequency'"
raise ValueError(msg)