# Copyright (C) 2026 Bence Göblyös
#
# This program is free software: you can redistribute it and/or modify it under
# terms of the GNU General Public License as published by the Free Software
# Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see https://www.gnu.org/licenses/.
"""
This module contains drivers for Thorlabs devices.
At the moment, this includes the PM100-series power meters.
Examples
--------
Below is a minimum working example showing how to use a PM100D power meter.
.. code-block:: python
import pyvisa
from Devices.Throlabs import PM100
rm = pyvisa.ResourceManager()
# Instantiate the PM100D with a predefined attenuation curve
pm = PM100(rm, 'USB0::0x1313::0x8078::P0017770::INSTR', attName='ppms-tap')
pm.wavelength = 530 # Set wavelength for an accurate reading
# When reading the power, the wavelength-dependent attenuation
# characteristics are automatically taken into account
print(f'Power: {pm.power * 1e3} mW')
"""
import numpy as np
import pandas as pd
import logging
import json
import sys
from pathlib import Path
if "sphinx" in sys.modules:
PROJECT_ROOT = Path(".")
else:
PROJECT_ROOT = Path(__file__).resolve().parents[1]
[docs]
class PM100():
"""
Driver class for ThorLabs PM100x power meter head units.
Manages instrument communication via PyVISA and applies an optional
polynomial wavelength-dependent attenuation calibration. Provides explicit
getters and property accessors for core SCPI functionality.
"""
def __init__(self, rm, address, attName=None, attConfig=(PROJECT_ROOT / "Config" / "PM100.json")):
"""
Initializes the power meter connection and loads optional calibration data.
Parameters
----------
rm : pyvisa.ResourceManager
The VISA resource manager instance.
address : str
The VISA resource address for the device.
attName : str, optional
The key inside the JSON configuration file corresponding to the
desired attenuation profile. Up to 9th order polynomials are accepted.
attConfig : str, default: "Config/PM100_attenuation.json"
Path to the JSON configuration file containing polynomial coefficients
for external attenuation/gain adjustments.
"""
# Set up logger
self.logger = logging.getLogger('instrumpy.PM100')
self.logger.propagate = True
self.logger.setLevel(logging.NOTSET)
self.logger.debug("Logger initialized.")
self.device = rm.open_resource(address)
self.device.read_termination = "\n"
self.setUnits('W')
self.poly = None
self.lower = 0
self.upper = 0
if attConfig is not None and attName is not None:
try:
with open(attConfig, "r") as file:
d = json.load(file)[attName]
coeffs = []
for i in range(10):
k = f'c{i}'
if k in d:
coeffs.append(d[k])
self.lower = d.get("cal_lower", 0)
self.upper = d.get("cal_upper", 0)
self.poly = np.polynomial.Polynomial(coeffs)
except Exception as e:
self.logger.warning(f"Configuration could not be loaded: {e}")
[docs]
def getID(self):
"""
Returns the instrument identification string.
Returns
-------
idn : str
Identification string (manufacturer, model, serial, firmware).
"""
return self.device.query("*IDN?")
[docs]
def getSensorID(self):
"""
Returns the connected sensor identification string.
Returns
-------
info : str
Sensor identification and configuration information.
"""
return self.device.query("SYST:SENS:IDN?")
[docs]
def getError(self):
"""
Returns the latest error code and message from the error queue.
Returns
-------
error : str
Formatted error message.
"""
return self.device.query("SYST:ERR?")
[docs]
def zero(self):
"""
Initiates zero adjustment (non-blocking).
"""
self.logger.info("Starting zero adjustment.")
self.device.write("SENS:CORR:COLL:ZERO:INIT")
[docs]
def getWavelength(self):
"""
Returns current operation wavelength.
Returns
-------
wavelength : float
Current wavelength in nm.
"""
return float(self.device.query("SENS:CORR:WAV?"))
[docs]
def setWavelength(self, val):
"""
Sets operation wavelength.
Parameters
----------
val : float
Wavelength in nm.
"""
if self.poly is not None:
if self.lower != 0 and val < self.lower:
self.logger.warning(f"The set wavelength ({val} nm) is below the calibrated minimum ({self.lower} nm). Attenuation calculations may be erroneous.")
if self.upper != 0 and val > self.upper:
self.logger.warning(f"The set wavelength ({val} nm) is above the calibrated maximum ({self.upper} nm). Attenuation calculations may be erroneous.")
self.device.write(f"SENS:CORR:WAV {int(val)}")
wavelength = property(fget=getWavelength, fset=setWavelength)
[docs]
def getAverage(self):
"""
Returns the number of samples averaged.
Returns
-------
count : int
Number of samples per measurement.
"""
return int(self.device.query("SENS:AVER:COUN?"))
[docs]
def setAverage(self, val):
"""
Sets the number of samples to average.
Parameters
----------
val : int
Number of samples.
"""
self.device.write(f"SENS:AVER:COUN {int(val)}")
average = property(fget=getAverage, fset=setAverage)
[docs]
def getFilterLowpass(self):
"""
Returns the state of the low-pass filter.
Returns
-------
enabled : bool
True if low-pass filter is enabled.
"""
return int(self.device.query("INP:PDI:FILT:LPAS:STAT?")) == 1
[docs]
def setFilterLowpass(self, state: bool):
"""
Enables or disables the low-pass filter.
Parameters
----------
state : bool
True to enable, False to disable.
"""
self.device.write(f"INP:PDI:FILT:LPAS:STAT {1 if state else 0}")
filterLowpass = property(fget=getFilterLowpass, fset=setFilterLowpass)
[docs]
def getThermopileAccelerator(self):
"""
Returns the state of the thermopile accelerator.
Returns
-------
active : bool
True if thermopile accelerator is active.
"""
return int(self.device.query("INP:THER:ACC:STAT?")) == 1
[docs]
def setThermopileAccelerator(self, state: bool):
"""
Enables or disables the thermopile accelerator.
Parameters
----------
state : bool
True to enable, False to disable.
"""
self.device.write(f"INP:THER:ACC:STAT {1 if state else 0}")
thermopileAccelerator = property(fget=getThermopileAccelerator, fset=setThermopileAccelerator)
[docs]
def setUnits(self, target):
"""
Sets measurement units.
Parameters
----------
target : str
Unit type ('W' or 'DBM').
"""
self.device.write(f"SENS:POW:UNIT {target}")
[docs]
def setAutoRange(self, state=True, mode="POW"):
"""
Enables or disables auto-ranging for a specific mode.
Parameters
----------
state : bool, default: True
True to enable, False to disable.
mode : str, default: "POW"
Measurement mode (e.g., "POW", "CURR", "VOLT").
"""
self.device.write(f"SENS:{mode}:RANG:AUTO {'ON' if state else 'OFF'}")
[docs]
def getPower(self, applyCalibration=True):
"""
Returns measured power.
Parameters
----------
applyCalibration : bool, default: True
Whether to apply the loaded polynomial attenuation profile.
Returns
-------
power : float
Measured optical power.
"""
raw = float(self.device.query("MEAS:POW?"))
if self.poly is not None and applyCalibration:
wl = self.getWavelength()
return raw * self.poly(wl)
else:
return raw
power = property(fget=getPower)
[docs]
def getEnergy(self):
"""
Returns measured pulse energy.
Returns
-------
energy : float
Measured pulse energy.
"""
return float(self.device.query("MEAS:ENER?"))
energy = property(fget=getEnergy)
[docs]
def getFrequency(self):
"""
Returns measured repetition frequency.
Returns
-------
frequency : float
Measured frequency in Hz.
"""
return float(self.device.query("MEAS:FREQ?"))
frequency = property(fget=getFrequency)
[docs]
def getTemperature(self):
"""
Returns current sensor temperature.
Returns
-------
temp : float
Temperature in Celsius.
"""
return float(self.device.query("MEAS:TEMP?"))
temperature = property(fget=getTemperature)