Source code for herosdevices.hardware.thorlabs.mdt69xb

"""Device driver for ThorLabs MDT69XB Piezo Controllers."""

import time

from herosdevices.core import DeviceCommandQuantity
from herosdevices.core.templates import SerialDeviceTemplate as SerialDevice
from herosdevices.helper import limits, mark_driver
from herosdevices.interfaces.atomiq import VoltageSource

MDT69XB_CHANNELS = {0: "x", 1: "y", 2: "z"}


def _remove_brackets(raw_str: str) -> str:
    """Remove brackets from piezo controller voltage return values."""
    return raw_str.strip("[]\r")


[docs] @mark_driver( info="Single Channel, Open-Loop Piezo Controller", product_page="https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=1191", state="beta", ) class MDT694B(SerialDevice): """Device driver for the MDT694B Single Channel, Open-Loop Piezo Controller. Args: address: Serial address of the device. Example: "/dev/ttyUSB0" timeout: timeout for read operations. """ voltage_x: float = DeviceCommandQuantity( command_set="xvoltage={}", command_get="xvoltage?", dtype=float, value_check_fun=limits(0, 150), unit="V", format_fun=_remove_brackets, ) min_voltage_x: float = DeviceCommandQuantity( command_set="xmin={}", command_get="xmin?", dtype=float, value_check_fun=limits(0, 150), unit="V" ) max_voltage_x: float = DeviceCommandQuantity( command_set="xmax={}", command_get="xmax?", dtype=float, value_check_fun=limits(0, 150), unit="V" ) def __init__(self, address: str, timeout: float = 1.0) -> None: SerialDevice.__init__( self, address, baudrate=115200, timeout=timeout, delays={"read_echo": 0.05}, read_line_termination=b"\r", write_line_termination=b"\r", ) self.connection.write("echo=0")
[docs] def initial_piezo_loop(self, voltage_start: float, voltage_stop: float, channel: int | str = 0) -> None: """Perform a piezo voltage loop to compensate for hysteresis effects. This function executes a voltage cycle (start -> stop -> start) on the specified piezo axis. Doing this before setting the target value helps compensate for the hysteresis behavior inherent in piezo actuators. Args: channel: The piezo axis to control. Can be either: - Integer (0, 1, 2) corresponding to x, y, z axes respectively - String ('x', 'y', 'z') directly specifying the axis For the one channel MDT694B, only x or 0 is valid. voltage_start: Starting voltage in volts for the hysteresis compensation loop voltage_stop: Stop voltage in volts for the hysteresis compensation loop Raises: ValueError: If channel is not a valid channel for the device """ channel_str: str = f"voltage_{MDT69XB_CHANNELS[channel] if isinstance(channel, int) else channel}" if not hasattr(self, channel_str): raise ValueError("Channel %s is not a valid channel for %s", channel, self) for voltage in [voltage_start, voltage_stop, voltage_start]: setattr(self, channel_str, voltage) time.sleep(2e-3) # 2ms delay between voltage changes to allow for settling time
[docs] @mark_driver( info="3-Channel, Open-Loop Piezo Controller", product_page="https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=1191", state="beta", ) class MDT693B(MDT694B): """Device driver for the MDT693B 3-Channel, Open-Loop Piezo Controller.""" voltage_y: float = DeviceCommandQuantity( command_set="yvoltage={}", command_get="yvoltage?", dtype=float, value_check_fun=limits(0, 150), unit="V", format_fun=_remove_brackets, ) min_voltage_y: float = DeviceCommandQuantity( command_set="ymin={}", command_get="ymin?", dtype=float, value_check_fun=limits(0, 150), unit="V" ) max_voltage_y: float = DeviceCommandQuantity( command_set="ymax={}", command_get="ymax?", dtype=float, value_check_fun=limits(0, 150), unit="V" ) voltage_z: float = DeviceCommandQuantity( command_set="zvoltage={}", command_get="zvoltage?", dtype=float, value_check_fun=limits(0, 150), unit="V", format_fun=_remove_brackets, ) min_voltage_z: float = DeviceCommandQuantity( command_set="zmin={}", command_get="zmin?", dtype=float, value_check_fun=limits(0, 150), unit="V" ) max_voltage_z: float = DeviceCommandQuantity( command_set="zmax={}", command_get="zmax?", dtype=float, value_check_fun=limits(0, 150), unit="V" )
[docs] @mark_driver( name="MDR69xB Channel", info="Single Channel Representation of a MDT69xB Piezo Driver", product_page="https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=1191", state="beta", ) class MDT69xBChannel(VoltageSource): """A single channel of a MDT69xB piezo driver. This class provides an :py:class:`herosdevices.interfaces.atomiq.VoltageSource` compatible interface to control a single channel of a MDT69xB piezo driver. Note: This class does not directly connect to the hardware but to another object given by the host_device argument which can also be a HERO running on another machine. It can be used to provide a universal interface which does not require setting a channel for every operation. Args: host_device: The host MDT69xB device (MDT693B or MDT694B). channel: The channel to control (can be int 0-2 or str 'x', 'y', 'z', depending on host_device) Raises: ValueError: If the channel is not valid for the host device """ def __init__(self, host_device: MDT693B | MDT694B, channel: int | str = "x") -> None: super().__init__() self.host_device = host_device self.channel: str = MDT69XB_CHANNELS[channel] if isinstance(channel, int) else channel if not hasattr(self.host_device, f"voltage_{self.channel}"): raise ValueError("Channel %s is not a valid channel for %s", channel, host_device) def _set_voltage(self, value: float) -> None: self.voltage = value @property def voltage(self) -> float: """Get or set the current voltage of the channel. Returns: The current voltage in volts """ return getattr(self.host_device, f"voltage_{self.channel}") @voltage.setter def voltage(self, value: float) -> None: """Set the current voltage of the channel. Args: value: The voltage to set in volts """ setattr(self.host_device, f"voltage_{self.channel}", value) @property def max_voltage(self) -> float: """Get or set the maximum voltage limit for the channel. Returns: The maximum voltage limit in volts """ return getattr(self.host_device, f"max_voltage_{self.channel}") @max_voltage.setter def max_voltage(self, value: float) -> None: """Set the maximum voltage limit for the channel. Args: value: The maximum voltage limit to set in volts """ setattr(self.host_device, f"max_voltage_{self.channel}", value) @property def min_voltage(self) -> float: """Get or set the minimum voltage limit for the channel. Returns: The minimum voltage limit in volts """ return getattr(self.host_device, f"min_voltage_{self.channel}") @min_voltage.setter def min_voltage(self, value: float) -> None: """Set the minimum voltage limit for the channel. Args: value: The minimum voltage limit to set in volts """ setattr(self.host_device, f"min_voltage_{self.channel}", value)
[docs] def initial_piezo_loop(self, voltage_start: float, voltage_stop: float) -> None: """Perform a piezo voltage loop to compensate for hysteresis effects. This function executes a voltage cycle (start -> stop -> start) on the specified piezo axis. Doing this before setting the target value helps compensate for the hysteresis behavior inherent in piezo actuators. Args: voltage_start: Starting voltage in volts for the hysteresis compensation loop voltage_stop: Stop voltage in volts for the hysteresis compensation loop Raises: ValueError: If channel is not a valid channel for the device """ self.host_device.initial_piezo_loop( voltage_start=voltage_start, voltage_stop=voltage_stop, channel=self.channel )