Skip to content

Commit

Permalink
Add powermeter functionality
Browse files Browse the repository at this point in the history
I changed the powermeter module from clade to pymeasure. I wrote a gui and logic module also.
  • Loading branch information
lange50 committed Mar 31, 2025
1 parent 4d4ea1f commit 869d11e
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 38 deletions.
18 changes: 17 additions & 1 deletion cfg/terascan.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
global:
# list of modules to load when starting
startup_modules: [photon_counts_time_average_gui, terascan_gui]
startup_modules: [photon_counts_time_average_gui, powermeter_gui, terascan_gui]

# Module server configuration for accessing qudi GUI/logic/hardware modules from remote clients
remote_modules_server:
Expand Down Expand Up @@ -44,6 +44,11 @@ gui:
swabian_timetagger: 'swabian_timetagger'
options:
ring_buffer_length_s: 5

powermeter_gui:
module.Class: 'powermeter.powermeter_gui.PowermeterGui'
connect:
powermeter_logic: powermeter_logic

logic:
terascan_logic:
Expand All @@ -59,6 +64,11 @@ logic:
mode_hop_overlap_med: 0.001 # in nm, from the SolsTiS control panel. This is how far back we go to discard data every time a mode hop occurs
mode_hop_overlap_fine: 0.00025 # ""

powermeter_logic:
module.Class: 'powermeter.powermeter_logic.PowermeterLogic'
connect:
powermeter: powermeter_thorlabs

# daq_reader_logic:
# module.Class: 'common.daq_reader_logic.DAQReaderLogic'
# connect:
Expand Down Expand Up @@ -117,6 +127,12 @@ hardware:
host_ip_addr: '192.168.1.225' # IP address of control computer
laser_ip_addr: '192.168.1.222' # IP address of laser
laser_port: 39900 # Port number to connect on

powermeter_thorlabs:
module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter'
options:
average_count: 5
update_period: 100 # in ms, hardware will emit data ~10 times/second

# Dummy Hardware:
# daq_reader_dummy:
Expand Down
53 changes: 53 additions & 0 deletions src/qudi/gui/powermeter/powermeter_gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from PySide2 import QtCore
from qudi.core.module import GuiBase
from qudi.core.connector import Connector
from qudi.core.statusvariable import StatusVar

from .powermeter_main_window import PowermeterMainWindow

class PowermeterGui(GuiBase):
"""
GUI module to display power meter readings with a multiplication factor.
Example config:
powermeter_gui:
module.Class: 'powermeter.powermeter_gui.PowermeterGui'
connect:
powermeter_logic: powermeter_logic
"""
# Connector to the logic module (which emits power readings)
_powermeter_logic = Connector(name='powermeter_logic', interface='PowermeterLogic')
# StatusVar to store the multiplication factor between sessions
_multiplication_factor = StatusVar(name='multiplication_factor', default=1.0)

def on_activate(self):
self._mw = PowermeterMainWindow()
# Initialize the main window's multiplication factor from the status variable.
self._mw.set_multiplication_factor(self._multiplication_factor)
# Set the spinbox value accordingly.
self._mw.mult_factor_spinbox.setValue(self._multiplication_factor)
# Connect spinbox changes to our handler.
self._mw.mult_factor_spinbox.valueChanged.connect(self._on_mult_factor_changed)
# Connect logic's power update signal to the main window update slot.
self._powermeter_logic().sigPowerUpdated.connect(
self._mw.update_power, QtCore.Qt.QueuedConnection
)
self.show()

def on_deactivate(self):
self._powermeter_logic().sigPowerUpdated.disconnect(self._mw.update_power)
self._mw.mult_factor_spinbox.valueChanged.disconnect(self._on_mult_factor_changed)
self._mw.close()

def show(self):
self._mw.show()
self._mw.raise_()

@QtCore.Slot(float)
def _on_mult_factor_changed(self, factor: float):
"""
Slot called when the multiplication factor is changed via the spinbox.
Updates the StatusVar and informs the main window.
"""
self._multiplication_factor = factor
self._mw.set_multiplication_factor(factor)
102 changes: 102 additions & 0 deletions src/qudi/gui/powermeter/powermeter_main_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from PySide2 import QtWidgets, QtCore, QtGui

class PowermeterMainWindow(QtWidgets.QMainWindow):
"""
Main window for displaying the Thorlabs Power Meter reading.
Features:
- Automatic unit conversion.
- A multiplication factor control (for correcting losses) in the lower right,
where the user types in the value (no increment arrows).
- Dynamic font scaling: as the window is resized, the power reading text scales.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('Thorlabs Power Meter')
self.resize(400, 180)

# Initialize multiplication factor (default 1.0)
self._mult_factor = 1.0

# Set up central widget and main layout
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
main_layout = QtWidgets.QVBoxLayout(central_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(5)

# Create power reading label with initial font
self.power_label = QtWidgets.QLabel("--", self)
self.power_label.setAlignment(QtCore.Qt.AlignCenter)
initial_font = QtGui.QFont("Arial", 24)
self.power_label.setFont(initial_font)
main_layout.addWidget(self.power_label)

# Add a stretch to push the following controls to the bottom
main_layout.addStretch()

# Create a horizontal layout for the multiplication factor controls
h_layout = QtWidgets.QHBoxLayout()
h_layout.addStretch() # Pushes items to the right side

self.mult_factor_label = QtWidgets.QLabel("Multiplication Factor:", self)
self.mult_factor_spinbox = QtWidgets.QDoubleSpinBox(self)
self.mult_factor_spinbox.setDecimals(4)
self.mult_factor_spinbox.setRange(0.0, 1000.0)
self.mult_factor_spinbox.setSingleStep(0.1)
# Remove the up/down arrows so the user types the value
self.mult_factor_spinbox.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
self.mult_factor_spinbox.setValue(self._mult_factor)

h_layout.addWidget(self.mult_factor_label)
h_layout.addWidget(self.mult_factor_spinbox)
main_layout.addLayout(h_layout)

@QtCore.Slot(float)
def update_power(self, power: float):
"""
Update the label text with the new power reading.
The raw power (in Watts) is multiplied by the multiplication factor, then
converted into a suitable unit (pW, nW, µW, mW, or W).
"""
corrected_power = power * self._mult_factor
abs_pwr = abs(corrected_power)

if abs_pwr < 1e-12:
display_val = corrected_power
display_unit = "W"
elif abs_pwr < 1e-9:
display_val = corrected_power * 1e12
display_unit = "pW"
elif abs_pwr < 1e-6:
display_val = corrected_power * 1e9
display_unit = "nW"
elif abs_pwr < 1e-3:
display_val = corrected_power * 1e6
display_unit = "µW"
elif abs_pwr < 1:
display_val = corrected_power * 1e3
display_unit = "mW"
else:
display_val = corrected_power
display_unit = "W"

self.power_label.setText(f"{display_val:.4f} {display_unit}")

def set_multiplication_factor(self, factor: float):
"""
Update the multiplication factor.
"""
self._mult_factor = factor

def resizeEvent(self, event):
"""
Dynamically scale the font size of the power reading label based on the
available width of the central widget.
Adjust the divisor (here, 10) to fine-tune scaling.
"""
available_width = self.centralWidget().width()
new_font_size = max(12, int(available_width / 10))
font = self.power_label.font()
font.setPointSize(new_font_size)
self.power_label.setFont(font)
super().resizeEvent(event)
113 changes: 76 additions & 37 deletions src/qudi/hardware/powermeter/thorlabs_power_meter.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,111 @@
from PySide2 import QtCore

from ThorlabsPM100 import ThorlabsPM100, USBTMC
# ref: https://github.com/clade/ThorlabsPM100
import pyvisa
from pymeasure.instruments.thorlabs import ThorlabsPM100USB

from qudi.core.configoption import ConfigOption
from qudi.util.mutex import RecursiveMutex
from qudi.interface.simple_powermeter_interface import SimplePowermeterInterface


class ThorlabsPowerMeter(SimplePowermeterInterface):
""" Hardware class for Thorlabs power meter. Assumes only one meter is attached
Example config for copy-paste:
"""
Hardware class for a Thorlabs power meter (via pymeasure).
Assumes exactly one meter is attached.
powermeter_thorlabs:
# This module assumes only one thorlabs meter is attached
module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter'
Example config for copy-paste:
options:
average_count: 5 # Internal samples to average in the power meter
update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible
powermeter_thorlabs:
module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter'
options:
average_count: 5 # Internal samples to average in the power meter
update_period: 0 # Timer interval in ms; 0 = as fast as possible
"""

_averages = ConfigOption(
name='average_count',
default=5
)

_update_interval = ConfigOption(name='update_period',
default=0,
missing='info')


# Run this in its own thread:
_averages = ConfigOption(name='average_count', default=5)
_update_interval = ConfigOption(name='update_period', default=0, missing='info')

# This module runs in its own thread:
_threaded = True

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__timer = None
self._thread_lock = RecursiveMutex()
self._meter = None

def on_activate(self):
inst = USBTMC()
self._meter = ThorlabsPM100(inst=inst)
self._meter.sense.power.dc.range.auto = "ON" # auto-range
self._meter.input.pdiode.filter.lpass.state = 0 # high bandwidth, 1 for low
"""
Called when Qudi activates the module. We open the first VISA resource
that appears, instantiate the PM100USB driver from pymeasure,
and set up a QTimer to poll periodically.
"""
rm = pyvisa.ResourceManager()
devices = rm.list_resources()

if not devices:
self.log.error("No VISA resources found. Cannot open Thorlabs PM100.")
return

# Open the first available device (adjust if you have multiple meters)
device = devices[0]
self.log.info(f"Opening resource: {device}")
self._meter = ThorlabsPM100USB(device)

# Example: set auto-range if supported
self._meter.auto_range = True

# Set the averaging if supported
self.set_average_count(self._averages)


# Start a timer to periodically read data
self.__timer = QtCore.QTimer()
self.__timer.timeout.connect(self.__data_update)
self.__timer.setSingleShot(False)
self.__timer.start(int(self._update_interval))


def on_deactivate(self):
if (self.__timer is not None):
"""
Called when Qudi deactivates the module.
Clean up the timer and close the device.
"""
if self.__timer is not None:
self.__timer.stop()
self.__timer.timeout.disconnect()
self.__timer = None

def get_power(self):
return self._meter.read

# If needed, close the device
if self._meter is not None:
try:
self._meter.shutdown() # Only if the pymeasure driver supports it
except AttributeError:
pass
self._meter = None

def get_power(self) -> float:
"""
Retrieve the current power reading from the device (in Watts).
"""
if self._meter is None:
return 0.0
return self._meter.power # from the pymeasure PM100USB driver

def set_average_count(self, count: int):
"""
Set the sample averaging on the power meter, if supported.
"""
self._averages = count
self._meter.sense.average.count = self._averages


if self._meter is not None:
# The actual pymeasure property might differ; adjust as needed.
try:
self._meter.average_count = count
except AttributeError:
pass # Not implemented by the driver?

def __data_update(self):
"""
Called by the QTimer to emit new data for any logic or GUI module
connected to sigNewData.
"""
with self._thread_lock:
data = self.get_power()
self.sigNewData.emit(data)
self.sigNewData.emit(data)
40 changes: 40 additions & 0 deletions src/qudi/logic/powermeter_logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-

from PySide2 import QtCore
from qudi.core.module import LogicBase
from qudi.core.connector import Connector
from qudi.util.mutex import RecursiveMutex

class PowermeterLogic(LogicBase):
"""
Logic module for displaying the Thorlabs power meter readings.
It connects to the hardware module, listens for new data, and re-emits
a dedicated signal to any GUI module.
"""

# Connector to the hardware module that implements SimplePowermeterInterface
_powermeter = Connector(name='powermeter', interface='SimplePowermeterInterface')

# Signal that the GUI can connect to for new power readings
sigPowerUpdated = QtCore.Signal(float)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._thread_lock = RecursiveMutex()

def on_activate(self):
""" Called when this logic module is activated by Qudi. """
# Connect hardware's new-data signal to our internal slot
self._powermeter().sigNewData.connect(self._on_new_data, QtCore.Qt.QueuedConnection)

def on_deactivate(self):
""" Called when this logic module is deactivated. """
# Safely disconnect signals
self._powermeter().sigNewData.disconnect(self._on_new_data)

@QtCore.Slot(float)
def _on_new_data(self, power: float):
""" Internal slot that receives new power readings from the hardware. """
with self._thread_lock:
# Simply re-emit the data for the GUI
self.sigPowerUpdated.emit(power)

0 comments on commit 869d11e

Please sign in to comment.