Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Thorlabs PM100 USB optical power meter #78

Merged
merged 8 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/comet/driver/thorlabs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .pm100 import PM100
91 changes: 91 additions & 0 deletions src/comet/driver/thorlabs/pm100.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from comet.driver.generic import Instrument, InstrumentError
from typing import Optional

__all__ = ["PM100"]


def parse_error(response: str):
code, message = [token.strip() for token in response.split(",")][:2]
return int(code), message.strip('"')


class PM100(Instrument):
"""Class for controlling Thorlabs PM100 USB power meters"""

WAVELENGTH_UV = 370
WAVELENGTH_IR = 1060

def identify(self) -> str:
return self.query("*IDN?")

def reset(self) -> None:
self.write("*RST")

def clear(self) -> None:
self.write("*CLS")

def next_error(self) -> Optional[InstrumentError]:
code, message = parse_error(self.query(":SYST:ERR:NEXT?"))
if code:
return InstrumentError(code, message)
return None

@property
def average_count(self) -> int:
"""Get current average count

Returns:
int: Average count
"""
return int(self.query("SENSe:AVERage:COUNt?"))

@average_count.setter
def average_count(self, value: int) -> None:
"""Set average count

One sample is around 3ms of measurement time.
Default is 100 samples.

Args:
value (int): _description_
"""
self.write(f"SENSe:AVERage:COUNt {value}")

@property
def wavelength(self) -> int:
"""Get calibration wavelength

Returns:
int: Calibration wavelength in nm
"""
return int(float(self.query("SENSe:CORRection:WAVelength?")))

@wavelength.setter
def wavelength(self, value: int) -> None:
"""Set calibration wavelength

Args:
wavelength (int): Wavelength in nm
"""

if value > 1100 or value < 350:
raise ValueError("Wavelength must be between 350 and 1100 nm")

self.write(f"SENSe:CORRection:WAVelength {value}")

def measure_power(self) -> float:
"""Measure power

Returns:
float: Power in W
"""

return float(self.query("MEASure:POWer?").strip())

# Helpers
def query(self, message: str) -> str:
return self.resource.query(message).strip()

def write(self, message: str) -> None:
self.resource.write(message)
self.query("*OPC?")
Empty file.
70 changes: 70 additions & 0 deletions src/comet/emulator/thorlabs/pm100.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Thorlabs PM100 USB power meter Emulator"""

import random

from typing import List

from comet.emulator import Emulator
from comet.emulator import message, run
from comet.emulator.utils import Error


__all__ = ["PM100Emulator"]


class PM100Emulator(Emulator):
IDENTITY: str = "Thorlabs,PM100USB,P2004525,1.4.0"

def __init__(self) -> None:
super().__init__()

self.error_queue: List[Error] = []

self.average_count = 100
self.wavelength = 370

@message(r"^\*IDN\?$")
def identify(self):
return self.IDENTITY

@message(r"^\*RST$")
def set_reset(self):
self.average_count = 100
self.wavelength = 370

@message(r"^\*CLS$")
def set_clear(self):
self.error_queue.clear()

@message(r"^:?SYST:ERR(?::NEXT)?\?$")
def get_system_error_next(self):
if self.error_queue:
error = self.error_queue.pop(0)
else:
error = Error(0, "no error")
return f'{error.code}, "{error.message}"'

@message(r"(?:^SENS(?:e)?)?:AVER(?:age)?:COUN(?:t)?\?$")
def get_average_count(self):
return self.average_count

@message(r"(?:^SENS(?:e)?)?:AVER(?:age)?:COUN(?:t)? (\d+)$")
def set_average_count(self, average_count):
self.average_count = average_count

@message(r"(?:^SENS(?:e)?)?:CORR(?:ection)?:WAV(?:elength)?\?$")
def get_wavelength(self):
return self.wavelength

@message(r"(?:^SENS(?:e)?)?:CORR(?:ection)?:WAV(?:elength)? (\d+)$")
def set_wavelength(self, wavelength):
self.wavelength = wavelength

@message(r"MEAS(?:ure)?(?::SCAL(?:ar)?)?(?::POW(?:er)?)?")
def measure_power(self):
power = random.uniform(1e-9, 2e-9)
return format(power, "E")


if __name__ == "__main__":
run(PM100Emulator())
66 changes: 66 additions & 0 deletions tests/test_driver_thorlabs_pm100.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import pytest

from comet.driver.thorlabs import PM100

from .test_driver import resource


@pytest.fixture
def driver(resource):
return PM100(resource)


def test_basic(driver, resource):
resource.buffer = ["Thorlabs,PM100USB,P2004525,1.4.0", "1", "1"]
assert driver.identify() == "Thorlabs,PM100USB,P2004525,1.4.0"
assert driver.reset() is None
assert driver.clear() is None


def test_average_count(driver, resource):

resource.buffer = ["100"]
assert driver.average_count == 100

resource.buffer = ["200"]
assert driver.average_count == 200

resource.buffer = ["1"]
driver.average_count = 1

assert resource.buffer == ["SENSe:AVERage:COUNt 1", "*OPC?"]


def test_wavelength(driver, resource):

resource.buffer = ["1", "1"]

driver.wavelength = driver.WAVELENGTH_UV
driver.wavelength = driver.WAVELENGTH_IR

assert resource.buffer == [
"SENSe:CORRection:WAVelength 370",
"*OPC?",
"SENSe:CORRection:WAVelength 1060",
"*OPC?",
]

resource.buffer = ["370", "1060", "500"]
assert driver.wavelength == driver.WAVELENGTH_UV
assert driver.wavelength == driver.WAVELENGTH_IR
assert driver.wavelength == 500

with pytest.raises(ValueError):
driver.wavelength = 340

with pytest.raises(ValueError):
driver.wavelength = 1200

with pytest.raises(ValueError):
driver.wavelength = -1


def test_measure_power(resource, driver):
resource.buffer = ["1e-9\n"]

assert driver.measure_power() == 1e-9
33 changes: 33 additions & 0 deletions tests/test_emulator_thorlabs_pm100.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import pytest

from comet.emulator.thorlabs.pm100 import PM100Emulator


@pytest.fixture
def emulator():
return PM100Emulator()


def test_basic(emulator):
assert emulator("*IDN?") == "Thorlabs,PM100USB,P2004525,1.4.0"
assert emulator("*CLS") is None
assert emulator("*RST") is None


def test_average_count(emulator):
assert emulator("SENSe:AVERage:COUNt?") == "100"
assert emulator("SENSe:AVERage:COUNt 200") is None
assert emulator("SENSe:AVERage:COUNt?") == "200"


def test_wavelength(emulator):
assert emulator("SENSe:CORRection:WAVelength?") == "370"
assert emulator("SENSe:CORRection:WAVelength 1060") is None
assert emulator("SENSe:CORRection:WAVelength?") == "1060"


def test_measure_power(emulator):

power = float(emulator("MEASure:SCALar:POWer"))
assert power >= 1e-9
assert power <= 2e-9
Loading