Module dlccontrol
Toptica DLCpro control
Convenience wrapper of Toptica Laser SDK for controlling a Toptica CTL with a DCLpro
Word of caution: This module controls potentially Class 4 lasers. Use is entirely on your own risk.
API documentation available here.
Docs can be built with python3 -m pdoc --html -o ./docs dlccontrol.py
The DLCcontrol class can read and control:
- laser current on/off
- wavelength setpoint for lasers that have this option
- laser diode setpoint for lasers that have this option
- analogue remote control settings (can control laser current and/or piezo simultaneously)- enable/disable
- select input channel
- set multiplier factor of the input voltage
 
- internal scan settings (both for scanning the piezo and laser
current)- scan start
- scan end
- scan offset
- scan amplitude
 
- user level (normal, maintenance, service)
- any other setting using the DLCcontrol.clientattribute
The class will check that the wavelength/temperature setpoint and internal scan
settings are within acceptable ranges (and raise a OutOfRangeError if not).
The module also provides some convenient dictionaries with all the settings it
can modify, these dictionaries can be saved with measurement data to make sure
all settings are recorded. The DLCcontrol class can dump these dicts to
json files.
Here are the parameters that can be saved, queried from the instrument and
printed with DLCcontrol.get_all_parameters(verbose=True):
-------------------------------------------------------
timestamp      : 2021-11-29 22:42:02.707762
scan:
 | enabled       : True
 | output channel: OutputChannel.PC
 | frequency     : 50.0000290562942
 | amplitude     : 21.0
 | offset        : 61.0
 | start         : 50.5
 | end           : 71.5
analogue remote:
 | cc:
 |  | enabled: False
 |  | factor : 10.0
 |  | signal : InputChannel.Fine1
 | pc:
 |  | enabled: False
 |  | factor : 10.0
 |  | signal : InputChannel.Fine2
wavelength:
 | wl setpoint: 1550.46
 | wl actual  : 1550.460841087153
temperatures:
 | temp setpoint: None
 | temp actual  : None
-------------------------------------------------------
Comparison to the Topica laser SDK
The module uses properties extensively (listed as Instance variables in the
docs), which means class attributes have setter and getter functions,
which can be used like this:
import dlccontrol as ctrl
with ctrl.DLCcontrol("xx.xx.xx.xx") as dlc:
    # Change wavelength or laser diode temperature depending on how the unit is
    # controlled
    if dlc.wl_setting_present:
        dlc.wavelength_setpoint = 1550
        actual_wl = dlc.wavelength_actual
    if dlc.temp_setting_present:
        dlc.temp_setpoint = 20
        actual_temp = dlc.temp_actual
    # Set up a the analogue remote control sweeping the current with the
    # on input Fine1
    dlc.remote_select = "CC"
    dlc.remote_signal = "Fine1"
    dlc.remote_factor = 10
    dlc.remote_enable = True
    # Use the internal voltage scan and gradually increase the scan amplitude
    dlc.scan_output_channel = "PC"
    initial_amplitude = dlc.scan_amplitude
    dlc.scan_frequency = 20
    for i in range(10):
        dlc.scan_amplitude = i
      dlc.scan_amplitude = initial_amplitude
Doing the same with the Toptica SDK would look like this (and this module is providing other features in addition to simplifying the syntax)
import toptica.lasersdk.dlcpro.v2_4_0 as toptica
import toptica.lasersdk.decop as decop
with toptica.DLCpro(toptica.NetworkConnection("xx.xx.xx.xx")) as dlc:
    try:
        dlc.laser1.ctl.wavelength_set.set(float(1550))
        actual_wl = dlc.laser1.ctl.wavelength_act.get()
    except decop.DecopError:
        pass
    try:
        dlc.laser1.dl.tc.temp_set(float(20))
        actual_temp = dlc.laser1.dl.tc.temp_act.get()
    except decop.DecopError:
        pass
    # Set up a the analogue remote control sweeping the current with the
    # on input Fine1
    dlc.laser1.dl.cc.external_input.signal.set(0)
    dlc.laser1.dl.cc.external_input.factor.set(10)
    dlc.laser1.dl.cc.external_input.enable.set(True)
    # Use the internal voltage scan and gradually increase the scan amplitude
    dlc.laser1.scan.output_channel.set(50)
    initial_amplitude = dlc.laser1.scan.amplitude.get()
    dlc.laser1.scan.frequency.set(20)
    for i in range(10):
        dlc.laser1.scan.amplitude.set(float(i))
    dlc.laser1.scan.amplitude.set(initial_amplitude)
Access any setting with client
If you want to access other settings than what the wrapper conveniently offers,
the DLCcontrol.client attribute is useful as it can give you access to any other setting:
import dlccontrol as ctrl
with ctrl.DLCcontrol("xx.xx.xx.xx") as dlc:
    print(dlc.client.get("serial-number"))
    # Need higher privilige to access the following commands
    dlc.set_user_level(1, "password from manual")
    dlc.client.set("laser1:dl:cc:current-clip", 250)
    dlc.client.set("laser1:dl:factory-settings:cc:current-clip", 250)
    dlc.client.exec("laser-common:store-all")
Note also the DLCcontrol.set_user_level() function to elevate the connection for access
to protected settings.
More examples are in the examples.py module.
Todos & known issues
- The upper frequency limit for internal scan is set very low, find out what the actual limits are for the voltage and current scan
- Handle limits for scan outputs to OutAandOutB(they can currently be used, just no checks on the range)
- Make update of interdependent scan settings update all relevant private dictionary entries
- Set parameters from dict/file
- Add property for setting the laser current when not scanning
- Tests would be helpful…
Source, contributions & license
The source is available on Github, please report issues there. Contributions are also welcome. The source code is licensed under the MIT license.
Changelog
- v0.2.0 Nov 2021: - Added support for temperature tuned lasers, automatic discovery of whether the laser is wavelength or temperature controlled
- Adding a clientattribute to theDLCcontrolclass to access any laser attribute
- Methods for setting and getting the user level for enabling change of restricted parameters
- Parameters dictionary now includes timestamp of the parameter set
- Black formatting
 
Expand source code
# -*- coding: utf-8 -*-
"""
.. include:: ./README.md
"""
import os
import time
import enum
import json
import argparse
import datetime
import numpy as np
import toptica.lasersdk as lasersdk
import toptica.lasersdk.dlcpro.v2_4_0 as dlcsdk
from toptica.lasersdk import decop, client
from typing import Union, Tuple, List, Any
IP = "192.168.100.100"
"""The default IP when initialising a ``DLCcontrol()``"""
MAINTENANCE_PSW = "CAUTION"
"""Factory default password for maintenance mode user level"""
class OutOfRangeError(ValueError):
    """Custom out of range errors for when a parameter is outside the permitted
    range"""
    def __init__(self, value: Any, parameter_name: str, permitted_range: List[Any]):
        self.value = value
        self.parameter_name = parameter_name
        self.range = permitted_range
        self.message = (
            f"{value} is not within the permitted "
            f"{parameter_name} range {permitted_range}"
        )
        super().__init__(self.message)
def _check_value(val: float, parameter_name: str, permitted_range: List[float]):
    """Check that a value is within a given range, raise error if not
    Raises
    ------
    OutOfRangeError
        If ``val`` is not within the two values of the ``permitted_range``
        list
    """
    if not permitted_range[0] <= val <= permitted_range[1]:
        raise OutOfRangeError(val, parameter_name, permitted_range)
def _print_dict(the_dict: dict, indent: int = 0, header: str = ""):
    """Recursive dictionary printing"""
    longest_key_len = len(max(the_dict.keys(), key=len))
    line = "-" * max(len(header), longest_key_len, 50)
    indent_spaces = " | " * indent
    if not indent:
        print("")
        if header:
            print(header)
        print(line)
    for key, val in the_dict.items():
        if isinstance(val, dict):
            print(f"{indent_spaces}{key}:")
            _print_dict(val, indent=(indent + 1))
        else:
            print(indent_spaces, end="")
            print(f"{key:<{longest_key_len}}: {val}")
    if not indent:
        print(line)
class OutputChannel(int, enum.Enum):  # int needed to avoid custom json serialiser
    """Output channel name to numeric value conversion"""
    PC = 50
    CC = 51
    OutA = 20
    OutB = 21
class InputChannel(int, enum.Enum):
    """Input channel name to numeric value conversion"""
    NotSelected = -3
    Fine1 = 0
    Fine2 = 1
    Fast3 = 2
    Fast4 = 3
# Dicts for converting between bools and text
_ON_OFF = {True: "on", False: "off"}
_ENABLED_DISABLED = {True: "enabled", False: "disabled"}
class DLCcontrol:
    """Control a Toptica DLCpro over an Ethernet connection
    Parameters
    ----------
    ip : str, default is the class attribute _ip (which defaults to the module constant ``IP``)
        IP address of the DLC unit
    open_on_init : bool, default ``True``
        Decide if ``open()`` should be called during the initialisation of
        the class object
    discover_wl_or_temp_control : bool, default ``True``
        Let the object automatically check if the laser is controlled by setting the
        wavelength or diode temperature
    force_wl_control_available : bool, default ``False``
        Force the object to assume the wavelength of the laser can be set
    force_temp_control_available : bool, default ``False``
        Force the object to assume the temperature of the laser diode can be set
    """
    _ip = IP
    _service_psw = "look in datasheet"
    """Custom SERVICE user level password unique to the DLCpro"""
    _is_open = False
    _remote_parameters = None
    _scan_parameters = None
    _lims = None
    calibration = None
    """MHz/mA or MHz/V calibration for the internal scan. Set by calling the
    ``freq_per_sec_internal_scan()`` method. After being set, the calibration
    will be kept in memory for future calls"""
    wl_control_available = False
    """Tells the object whether the laser is controlled with a wavelength setpoint"""
    temp_control_available = False
    """Tells the object whether the laser is controlled with a temperature setpoint"""
    client = None
    """After opening the connection the client can be used to control any setting
    for the DLCpro, for instance `client.set("laser1:dl:cc:current-act", 10)`"""
    def __init__(
        self,
        ip: Union[str, None] = None,
        open_on_init: bool = True,
        discover_wl_or_temp_control: bool = True,
        force_wl_control_available: bool = False,
        force_temp_control_available: bool = False,
    ):
        self.discover_wl_or_temp_control = discover_wl_or_temp_control
        if force_wl_control_available:
            self.wl_control_available = True
            self.discover_wl_or_temp_control = False
        if force_temp_control_available:
            self.temp_control_available = True
            self.discover_wl_or_temp_control = False
        if ip is not None:
            self._ip = ip
        self.connection = dlcsdk.NetworkConnection(self._ip)
        self.client = client.Client(self.connection)
        self.dlc = dlcsdk.DLCpro(self.connection)
        if open_on_init:
            self.open()
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
    def open(self):
        """Open the connection to the laser and get all the parameters of the
        laser required to use the class"""
        self.dlc.open()
        self._is_open = True
        # Make sure all class attributes are up to date
        if self.discover_wl_or_temp_control:
            self._discover_control()
        self.get_limits_from_dlc()
        self.get_scan_parameters()
        self.get_remote_parameters()
        self._update_scan_range_attribute()
    def close(self):
        """Close the connection to the DLC"""
        if self._is_open:
            self.dlc.close()
    def set_user_level(
        self, level: int, password: str = "default", verbose: bool = True
    ):
        """Sets the user level privileges of the client *connection*, does not change
        the user level on the DLCpro console
        Parameters
        ----------
        level : int
            User level where 3 is normal, 2 is maintenance, 1 is service
        password : str
            Password for accessing this level. Using `default` will select the correct
            passoword for level 3 and 2. For level 1, the password is unique to the
            DLCpro and can be found in the datasheet.
        """
        if password == "default":
            if level == 1:
                print(
                    "CAUTION: This is SERVICE level user, protected by a custom password for each unit."
                )
                inp = input("Do you really really want to proceed? [y/N] ")
                if inp.lower() != "y":
                    print("Aborting user level change")
                    return
                password = self._service_psw
            elif level == 2:
                password = MAINTENANCE_PSW
        ul = decop.UserLevel(level)
        result = self.dlc.change_ul(ul, password)
        if verbose:
            print(f"New user level: {result.name}")
    def get_user_level(self) -> decop.UserLevel:
        """Gets the user level privileges of the *connection*, does not reflect the
        user level on the DLCpro console"""
        return decop.UserLevel(self.client.get("ul"))
    # Limits and settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
    def _discover_control(self, verbose=False):
        # Check for wavelength control
        try:
            self.dlc.laser1.ctl.wavelength_min.get()
            self.wl_control_available = True
        except decop.DecopError as e:
            errormsg = e.args[0]
            if not ("-20" in errormsg or "unavailable" in errormsg):
                print(
                    f"Unknown error '{e}' when discovering if wavelength control is available"
                )
            self.temp_control_available = False
        # Check for laser diode temperature control
        try:
            self.dlc.laser1.dl.tc.temp_set_min.get()
            self.temp_control_available = True
        except decop.DecopError as e:
            errormsg = e.args[0]
            if not ("-20" in errormsg or "unavailable" in errormsg):
                print(
                    f"Unknown error '{e}' when discovering if laser diode temperature control is available"
                )
            self.temp_control_available = False
        if verbose:
            print(f"The laser has wavelength control: {self.wl_control_available}")
            print(
                f"The laser has diode temperature control: {self.temp_control_available}"
            )
    def get_limits_from_dlc(self, verbose=False) -> dict:
        """Query the laser for the wavelength, piezo voltage, current and
        scan frequency limits, and populate the ``_lims`` dict attribute
        Returns
        -------
        self._lims : dict
            The limits
        """
        self._lims = {
            "vmin": self.dlc.laser1.dl.pc.voltage_min.get(),
            "vmax": self.dlc.laser1.dl.pc.voltage_max.get(),
            "cmin": 0.0,
            "cmax": self.dlc.laser1.dl.cc.current_clip.get(),
            "fmin": 0.02,
            "fmax": 400,  # cannot find max in manual
            "tmin": None,
            "tmax": None,
            "wlmin": None,
            "wlmax": None,
        }
        if self.wl_control_available:
            self._lims.update(
                {
                    "wlmin": self.dlc.laser1.ctl.wavelength_min.get(),
                    "wlmax": self.dlc.laser1.ctl.wavelength_max.get(),
                }
            )
        if self.temp_control_available:
            self._lims.update(
                {
                    "tmin": self.dlc.laser1.dl.tc.temp_set_min.get(),
                    "tmax": self.dlc.laser1.dl.tc.temp_set_max.get(),
                }
            )
        if verbose:
            _print_dict(self._lims)
        return self._lims
    def get_scan_parameters(self, verbose: bool = False) -> dict:
        """Query the laser for the current scan settings, populate the
        ``_scan_parameters`` dict attribute
        Returns
        -------
        self._scan_parameters : dict
            All parameters for the internal scan
        """
        self._scan_parameters = {
            "enabled": self.scan_enabled,
            "output channel": self.scan_output_channel,
            "frequency": self.scan_frequency,
            "amplitude": self.scan_amplitude,
            "offset": self.scan_offset,
            "start": self.scan_start,
            "end": self.scan_end,
        }
        if verbose:
            _print_dict(self._scan_parameters)
        return self._scan_parameters
    @property
    def _vrange(self):
        return self._lims["vmin"], self._lims["vmax"]
    @property
    def _crange(self):
        return self._lims["cmin"], self._lims["cmax"]
    @property
    def _trange(self):
        return self._lims["tmin"], self._lims["tmax"]
    @property
    def _wlrange(self):
        return self._lims["wlmin"], self._lims["wlmax"]
    def _update_scan_range_attribute(self, channel: Union[None, OutputChannel] = None):
        if channel is None:
            channel = self._scan_parameters["output channel"]
        if channel == OutputChannel.CC:
            self._scan_range = self._crange
        elif channel == OutputChannel.PC:
            self._scan_range = self._vrange
        else:
            self._scan_range = [-np.inf, np.inf]
            print("(!) Warning: Scan range for OutA and OutB is not limted", flush=True)
    def get_remote_parameters(self, verbose: bool = False) -> dict:
        """Query the laser for the analogue remote control settings, and
        populate the ``_remote_parameters`` dict attribute
        Returns
        -------
        self._scan_parameters : dict
            All parameters for the analogue remote control
        """
        self._remote_parameters = {}
        for unit in ("cc", "pc"):
            self.remote_select = unit
            self._remote_parameters[unit] = {
                "enabled": self.remote_enabled,
                "factor": self.remote_factor,
                "signal": self.remote_signal,
            }
        if verbose:
            _print_dict(self._remote_parameters)
        return self._remote_parameters
    def get_all_parameters(self, verbose: bool = False) -> dict:
        """Returns an updated dictionary of all the parameters that can be set
        with the module
        Returns
        -------
        dict
            A nested dictionary with the parameters
        """
        timestamp = datetime.datetime.now()
        wls = {
            "wl setpoint": self.wavelength_setpoint,
            "wl actual": self.wavelength_actual,
        }
        temps = {"temp setpoint": self.temp_setpoint, "temp actual": self.temp_actual}
        # Updating scan parameters as they are interdependent
        params = {
            "timestamp": str(timestamp),
            "scan": self.get_scan_parameters(),
            "analogue remote": self._remote_parameters,
            "wavelength": wls,
            "temperature": temps,
        }
        if verbose:
            _print_dict(params)
        return params
    def save_parameters(self, fname: str):
        """Grab an updated set of laser parameters and save to a ``json`` file
        Raises
        ------
        RuntimeError
            If a file with name `fname` already exists
        """
        params = self.get_all_parameters()
        if not fname.endswith(".json"):
            fname += ".json"
        if os.path.exists(fname):
            raise RuntimeError(f"File '{fname}' already exists")
        with open(fname, "w") as outfile:
            json.dump(params, outfile, indent="  ")
    @staticmethod
    def read_parameters(fname: str, verbose: bool = True) -> dict:
        """Read (but not set!) parameters from json file"""
        if not fname.endswith(".json"):
            fname += ".json"
        with open(fname) as json_file:
            params = json.load(json_file)
        if verbose:
            _print_dict(params)
        return params
    def set_parameters(self, params: dict):
        """*Not yet implemented*
        The idea is to be able to use the parameters in a dict and set them
        accordingly"""
        raise NotImplementedError("Still to be implemented")
    def verbose_emission_status(self):
        """Print the emission status of the laser, for example
        ```
        Emission button is ENABLED
        Laser current is DISABLED
        Therefore, emission is ON
        ```
        """
        print(f"Emission button is {_ENABLED_DISABLED[self.emission_button]}")
        print(f"Laser current is {_ENABLED_DISABLED[self.current_enabled]}")
        print(f"Therefore, emission is {_ON_OFF[self.emission]}")
    def freq_per_sec_internal_scan(self, calibration: float = None) -> float:
        """Calculate frequency span per second for the laser in MHz per second
        from the scan parameters
        Parameters
        ----------
        calibration : float
            [MHz/mA or MHz/V]
        """
        params = self.get_scan_parameters()
        scan_freq = params["frequency"]
        peak_to_peak = params["amplitude"]
        if calibration is not None:
            self.calibration = calibration
        return freq_per_sec(
            scan_freq, peak_to_peak, scaling=1, calibration=self.calibration
        )
    # Emission properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
    @property
    def emission(self) -> bool:
        """Emission status of the DLC (read only)"""
        return self.dlc.emission.get()
    @property
    def emission_button(self) -> bool:
        """Status of the emission button of the DLC (read only)"""
        return self.dlc.emission_button_enabled.get()
    @property
    def current_enabled(self) -> bool:
        """Status of the current to the laser"""
        return self.dlc.laser1.dl.cc.enabled.get()
    @current_enabled.setter
    def current_enabled(self, val: bool):
        """Sneaky way to control emission on/off provided the button on the
        DLC is enabled"""
        if val and not self.emission_button:
            print("(!) Emission button on DLC not enabled, so cannot enable emission")
        self.dlc.laser1.dl.cc.enabled.set(val)
    # Wavelength properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
    @property
    def wavelength_actual(self) -> float:
        """The actual wavelength of the laser (read only)"""
        if not self.wl_control_available:
            return None
        return self.dlc.laser1.ctl.wavelength_act.get()
    @property
    def wavelength_setpoint(self) -> float:
        """The setpont of the laser wavelength"""
        if not self.wl_control_available:
            return None
        return self.dlc.laser1.ctl.wavelength_set.get()
    @wavelength_setpoint.setter
    def wavelength_setpoint(self, val: float):
        if not self.wl_control_available:
            raise RuntimeError(
                "Cannot set wavelength when `wl_control_available` is False"
            )
        if val is None:
            return
        val = float(val)
        if self._wlrange[0] is None:
            self.get_limits_from_dlc()
        _check_value(val, "wavelength setpoint", self._wlrange)
        self.dlc.laser1.ctl.wavelength_set.set(val)
    ## Temperature properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
    @property
    def temp_actual(self) -> float:
        """The actual temperature of the laser diode (read only)"""
        if not self.temp_control_available:
            return None
        return self.dlc.laser1.dl.tc.temp_act.get()
    @property
    def temp_setpoint(self) -> float:
        """The setpoint of the laser diode temperature"""
        if not self.temp_control_available:
            return None
        return self.dlc.laser1.dl.tc.temp_set.get()
    @temp_setpoint.setter
    def temp_setpoint(self, val: float):
        if not self.temp_control_available:
            raise RuntimeError(
                "Cannot set diode temperature `temp_control_available` is False"
            )
        if val is None:
            return
        val = float(val)
        if self._trange[0] is None:
            self.get_limits_from_dlc()
        _check_value(val, "temperature setpoint", self._trange)
        self.dlc.laser1.dl.tc.temp_set.set(val)
    # Remote properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
    @property
    def remote_select(self) -> Tuple[str, Any]:
        """Analogue Remote Control for both the DLCpro's current (cc)
        and voltage (pc) can be used simultaneously. With this class, both can
        be used simultaneously, with this select property choosing which remote
        is receiveing the commands at any given time
        Example
        -------
            with DLCcontrol(ip) as dlc:
                # Choose to set the ARC for the current
                dlc.remote_select = "CC"
                # Decide its input..
                dlc.remote_signal = "Fine1"
                # ..and enable it
                dlc.remote_enable = True
                # Now move to the ARC for the piezo..
                dlc.remote_select = "PC"
                # ..and choose some settings for it
                dlc.remote_signal = "Fast3"
                dlc.remote_enable = True
        """
        return self._remote_str, self._remote_unit
    @remote_select.setter
    def remote_select(self, select: str):
        """
        select : {"pc", "cc"}
        """
        if select.lower() == "cc":
            self._remote_str = "cc"
            self._remote_unit = self.dlc.laser1.dl.cc.external_input
        elif select.lower() == "pc":
            self._remote_str = "pc"
            self._remote_unit = self.dlc.laser1.dl.pc.external_input
        else:
            raise ValueError(
                f"select must be either 'pc' nor 'cc' (tried using '{select}')"
            )
    @property
    def remote_enabled(self) -> bool:
        """Status of the chosen remote"""
        return self._remote_unit.enabled.get()
    @remote_enabled.setter
    def remote_enabled(self, val: bool):
        self._remote_unit.enabled.set(val)
        self._remote_parameters[self._remote_str]["enabled"] = val
    @property
    def remote_signal(self) -> InputChannel:
        """The input port the chosen remote uses"""
        num = self._remote_unit.signal.get()
        return InputChannel(num)
    @remote_signal.setter
    def remote_signal(self, val: Union[InputChannel, str]):
        """Choose which output channel to use for the ARC
        val : {"Fine1", "Fine2", "Fast3", "Fast4",
               InputChannel.Fine1, InputChannel.Fine2,
               InputChannel.Fast3, InputChannel.Fast4}"""
        try:
            if isinstance(val, InputChannel):
                num = val.value
            elif isinstance(val, str):
                num = InputChannel[val.title()].value
            else:
                raise KeyError
        except KeyError:
            raise ValueError(
                "Input channel must be one of 'Fine1', 'Fine2', "
                f"'Fast3', 'Fast4', or an InputChannel (tried with '{val}')"
            ) from KeyError
        self._remote_unit.signal.set(num)
        self._remote_parameters[self._remote_str]["signal"] = InputChannel(num)
    @property
    def remote_factor(self) -> float:
        """The numerical factor the remote signal is multiplied with before used
        as the current or piezo control"""
        return self._remote_unit.factor.get()
    @remote_factor.setter
    def remote_factor(self, val: float):
        val = float(val)
        self._remote_unit.factor.set(val)
        self._remote_parameters[self._remote_str]["factor"] = val
    # Scan properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
    @property
    def scan_enabled(self) -> bool:
        """Internal scan on/off"""
        return self.dlc.laser1.scan.enabled.get()
    @scan_enabled.setter
    def scan_enabled(self, val: bool):
        self.dlc.laser1.scan.enabled.set(val)
        self._scan_parameters["enabled"] = val
    @property
    def scan_output_channel(self) -> OutputChannel:
        """Internal scan output channel. It can be directed to the
        piezo or laser current directly, or to the output BNCs on the DLC"""
        num = self.dlc.laser1.scan.output_channel.get()
        return OutputChannel(num)
    @scan_output_channel.setter
    def scan_output_channel(self, val: Union[OutputChannel, str]):
        """The internal scan can only act on eiter piezo or the current at any
        given time, or be directed to the DLC BNCs
        val : {"CC", "PC", OutputChannel.CC, OutputChannel.PC}"""
        try:
            if isinstance(val, OutputChannel):
                num = val.value
            elif isinstance(val, str):
                num = OutputChannel[val.upper()].value
            else:
                raise KeyError
        except KeyError:
            raise ValueError(
                "Channel must be 'CC', 'PC', OutputChannel.CC, or "
                f"OutputChannel.PC (tried with '{val}')"
            ) from KeyError
        self.dlc.laser1.scan.output_channel.set(num)
        self._scan_parameters["scan_output_channel"] = OutputChannel(num)
        self._update_scan_range_attribute(OutputChannel(num))
    @property
    def scan_frequency(self) -> float:
        """Internal scan frequency"""
        return self.dlc.laser1.scan.frequency.get()
    @scan_frequency.setter
    def scan_frequency(self, val: float):
        val = float(val)
        _check_value(val, "scan frequency", (self._lims["fmin"], self._lims["fmax"]))
        self.dlc.laser1.scan.frequency.set(val)
        self._scan_parameters["frequency"] = val
    @property
    def scan_amplitude(self) -> float:
        """Internal scan amplitude"""
        return self.dlc.laser1.scan.amplitude.get()
    @scan_amplitude.setter
    def scan_amplitude(self, val: float):
        val = float(val)
        offset = self.scan_offset
        new_range = [offset - val / 2, offset + val / 2]
        if min(new_range) < self._scan_range[0] or max(new_range) > self._scan_range[1]:
            raise OutOfRangeError(new_range, "scan", self._scan_range)
        self.dlc.laser1.scan.amplitude.set(val)
        self._scan_parameters["amplitude"] = val
    @property
    def scan_offset(self) -> float:
        """Internal scan offset value"""
        return self.dlc.laser1.scan.offset.get()
    @scan_offset.setter
    def scan_offset(self, val: float):
        val = float(val)
        amplitude = self.scan_amplitude
        new_range = [val - amplitude / 2, val + amplitude / 2]
        if min(new_range) < self._scan_range[0] or max(new_range) > self._scan_range[1]:
            raise OutOfRangeError(new_range, "scan", self._scan_range)
        self.dlc.laser1.scan.offset.set(val)
        self._scan_parameters["offset"] = val
    @property
    def scan_start(self) -> float:
        """Internal scan start value"""
        return self.dlc.laser1.scan.start.get()
    @scan_start.setter
    def scan_start(self, val: float):
        val = float(val)
        _check_value(val, "scan start", self._scan_range)
        self.dlc.laser1.scan.start.set(val)
        self._scan_parameters["start"] = val
    @property
    def scan_end(self) -> float:
        """Interal scan end value"""
        return self.dlc.laser1.scan.end.get()
    @scan_end.setter
    def scan_end(self, val: float):
        val = float(val)
        _check_value(val, "scan end", self._scan_range)
        self.dlc.laser1.scan.end.set(val)
        self._scan_parameters["end"] = val
def freq_per_sec(
    scan_freq: float, peak_to_peak: float, scaling: float, calibration: float
) -> float:
    """Calculate frequency sweep per second for the laser in MHz per second
    when using a triangular sweep function
    Parameters
    ----------
    scan_freq : float
        [Hz]
    peak_to_peak : float
        [Vpp]
    scaling : float
        [mA/V or V/V]
    calibration : float
        [MHz/mA or MHz/V]
    """
    scan_period = 1 / (2 * scan_freq)  # sec
    # (division by two because of triangle wave and hence in practice
    #  sweeping double the speed)
    scaled_ptp = peak_to_peak * scaling  # mA or V
    return scaled_ptp * calibration / scan_period  # MHz/second
def freq_per_sec_from_params(params: dict, calibration: float) -> float:
    """Calculate frequency sweep per second for the laser in MHz per second due
    to the internal scan using a params dictionary
    Parameters
    ----------
    params : dict
        As provided by ``DLCcontrol.get_all_parameters()`` or a ``json`` file
    calibration : float
        [MHz/mA or MHz/V]
    """
    scan_freq = params["scan"]["frequency"]
    peak_to_peak = params["scan"]["amplitude"]
    return freq_per_sec(scan_freq, peak_to_peak, scaling=1, calibration=calibration)
# An example programme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
def step_through_scan_range(ip=IP, steps: int = 20, dlc: DLCcontrol = None):
    """A simple programme: Step through the internal voltage/current
    scan range currently in use
    Parameters
    ----------
    ip : str
        IP of the DLC if a connection should be opened
    steps : int, default 20
        The number of steps to divide the amplitude into
    dlc : DLCcontrol, optional
        If a ``DLCcontrol`` object is provided, a new one will not be created
    """
    if dlc is None:
        dlc = DLCcontrol(ip)
        close_flag = True
    else:
        close_flag = False
    # Read initial values
    initial_end = dlc.scan_end
    initial_offset = dlc.scan_offset
    initial_amplitude = dlc.scan_amplitude
    # Define range to scan
    step_range = np.linspace(0, -initial_amplitude, steps)
    try:
        dlc.scan_amplitude = 0
        for i, change in enumerate(step_range):
            try:
                print(f"{i}: change to {initial_end+change:.3f}V")
                try:
                    dlc.scan_offset = initial_end + change
                except OutOfRangeError as err:
                    print(err)
                    break
                time.sleep(1)
            except KeyboardInterrupt:
                print("Stopping scan")
                break
    finally:
        print("Restore initial state")
        dlc.scan_offset = initial_offset
        dlc.scan_amplitude = initial_amplitude
        if close_flag:
            dlc.close()
def command_line_programme():
    """Command line use of the module: run ``python dlccontrol.py -h`` to see
    the options"""
    parser = argparse.ArgumentParser(description="A few useful laser control funtions")
    parser.add_argument(
        "-i",
        "--ip",
        type=str,
        default="",
        help=f"The ip of the laser (defaults to {IP})",
    )
    parser.add_argument(
        "-e",
        "--emission-status",
        dest="emission",
        action="store_true",
        help="Print the emission status of the device",
    )
    parser.add_argument(
        "-p", "--parameters", action="store_true", help="Print the laser parameters"
    )
    parser.add_argument(
        "-s",
        "--save-filename",
        dest="fname",
        type=str,
        default=None,
        help=("Save all laser parameters to a json file to filename"),
    )
    parser.add_argument(
        "-f",
        "--folder",
        type=str,
        default="./",
        help=(
            "Select a folder for storing saved files if different "
            "from the folder where the script is exectuted"
        ),
    )
    parser.add_argument(
        "-n",
        "--steps",
        type=int,
        default=0,
        help=("Scan discretely through the current laser span in <STEPS>"),
    )
    args = parser.parse_args()
    ip = args.ip if args.ip else IP
    with DLCcontrol(ip) as dlc:
        if args.emission:
            dlc.verbose_emission_status()
        if args.parameters:
            dlc.get_all_parameters(verbose=True)
        if args.fname is not None:
            dlc.save_parameters(args.folder + args.fname)
        if args.steps:
            step_through_scan_range(ip, args.steps, dlc)
if __name__ == "__main__":
    command_line_programme()Global variables
- var IP
- 
The default IP when initialising a DLCcontrol
- var MAINTENANCE_PSW
- 
Factory default password for maintenance mode user level 
Functions
- def command_line_programme()
- 
Command line use of the module: run python dlccontrol.py -hto see the optionsExpand source codedef command_line_programme(): """Command line use of the module: run ``python dlccontrol.py -h`` to see the options""" parser = argparse.ArgumentParser(description="A few useful laser control funtions") parser.add_argument( "-i", "--ip", type=str, default="", help=f"The ip of the laser (defaults to {IP})", ) parser.add_argument( "-e", "--emission-status", dest="emission", action="store_true", help="Print the emission status of the device", ) parser.add_argument( "-p", "--parameters", action="store_true", help="Print the laser parameters" ) parser.add_argument( "-s", "--save-filename", dest="fname", type=str, default=None, help=("Save all laser parameters to a json file to filename"), ) parser.add_argument( "-f", "--folder", type=str, default="./", help=( "Select a folder for storing saved files if different " "from the folder where the script is exectuted" ), ) parser.add_argument( "-n", "--steps", type=int, default=0, help=("Scan discretely through the current laser span in <STEPS>"), ) args = parser.parse_args() ip = args.ip if args.ip else IP with DLCcontrol(ip) as dlc: if args.emission: dlc.verbose_emission_status() if args.parameters: dlc.get_all_parameters(verbose=True) if args.fname is not None: dlc.save_parameters(args.folder + args.fname) if args.steps: step_through_scan_range(ip, args.steps, dlc)
- def freq_per_sec(scan_freq: float, peak_to_peak: float, scaling: float, calibration: float) ‑> float
- 
Calculate frequency sweep per second for the laser in MHz per second when using a triangular sweep function Parameters- scan_freq:- float
- [Hz]
- peak_to_peak:- float
- [Vpp]
- scaling:- float
- [mA/V or V/V]
- calibration:- float
- [MHz/mA or MHz/V]
 Expand source codedef freq_per_sec( scan_freq: float, peak_to_peak: float, scaling: float, calibration: float ) -> float: """Calculate frequency sweep per second for the laser in MHz per second when using a triangular sweep function Parameters ---------- scan_freq : float [Hz] peak_to_peak : float [Vpp] scaling : float [mA/V or V/V] calibration : float [MHz/mA or MHz/V] """ scan_period = 1 / (2 * scan_freq) # sec # (division by two because of triangle wave and hence in practice # sweeping double the speed) scaled_ptp = peak_to_peak * scaling # mA or V return scaled_ptp * calibration / scan_period # MHz/second
- def freq_per_sec_from_params(params: dict, calibration: float) ‑> float
- 
Calculate frequency sweep per second for the laser in MHz per second due to the internal scan using a params dictionary Parameters- params:- dict
- As provided by DLCcontrol.get_all_parameters()or ajsonfile
- calibration:- float
- [MHz/mA or MHz/V]
 Expand source codedef freq_per_sec_from_params(params: dict, calibration: float) -> float: """Calculate frequency sweep per second for the laser in MHz per second due to the internal scan using a params dictionary Parameters ---------- params : dict As provided by ``DLCcontrol.get_all_parameters()`` or a ``json`` file calibration : float [MHz/mA or MHz/V] """ scan_freq = params["scan"]["frequency"] peak_to_peak = params["scan"]["amplitude"] return freq_per_sec(scan_freq, peak_to_peak, scaling=1, calibration=calibration)
- def step_through_scan_range(ip='192.168.100.100', steps: int = 20, dlc: DLCcontrol = None)
- 
A simple programme: Step through the internal voltage/current scan range currently in use Parameters- ip:- str
- IP of the DLC if a connection should be opened
- steps:- int, default- 20
- The number of steps to divide the amplitude into
- dlc:- DLCcontrol, optional
- If a DLCcontrolobject is provided, a new one will not be created
 Expand source codedef step_through_scan_range(ip=IP, steps: int = 20, dlc: DLCcontrol = None): """A simple programme: Step through the internal voltage/current scan range currently in use Parameters ---------- ip : str IP of the DLC if a connection should be opened steps : int, default 20 The number of steps to divide the amplitude into dlc : DLCcontrol, optional If a ``DLCcontrol`` object is provided, a new one will not be created """ if dlc is None: dlc = DLCcontrol(ip) close_flag = True else: close_flag = False # Read initial values initial_end = dlc.scan_end initial_offset = dlc.scan_offset initial_amplitude = dlc.scan_amplitude # Define range to scan step_range = np.linspace(0, -initial_amplitude, steps) try: dlc.scan_amplitude = 0 for i, change in enumerate(step_range): try: print(f"{i}: change to {initial_end+change:.3f}V") try: dlc.scan_offset = initial_end + change except OutOfRangeError as err: print(err) break time.sleep(1) except KeyboardInterrupt: print("Stopping scan") break finally: print("Restore initial state") dlc.scan_offset = initial_offset dlc.scan_amplitude = initial_amplitude if close_flag: dlc.close()
Classes
- class DLCcontrol (ip: Optional[str] = None, open_on_init: bool = True, discover_wl_or_temp_control: bool = True, force_wl_control_available: bool = False, force_temp_control_available: bool = False)
- 
Control a Toptica DLCpro over an Ethernet connection Parameters- ip:- str, default- is the class attribute _ip (which defaults to the module constant- <a title="dlccontrol.IP" href="#dlccontrol.IP">IP</a>)
- IP address of the DLC unit
- open_on_init:- bool, default- True
- Decide if open()should be called during the initialisation of the class object
- discover_wl_or_temp_control:- bool, default- True
- Let the object automatically check if the laser is controlled by setting the wavelength or diode temperature
- force_wl_control_available:- bool, default- False
- Force the object to assume the wavelength of the laser can be set
- force_temp_control_available:- bool, default- False
- Force the object to assume the temperature of the laser diode can be set
 Expand source codeclass DLCcontrol: """Control a Toptica DLCpro over an Ethernet connection Parameters ---------- ip : str, default is the class attribute _ip (which defaults to the module constant ``IP``) IP address of the DLC unit open_on_init : bool, default ``True`` Decide if ``open()`` should be called during the initialisation of the class object discover_wl_or_temp_control : bool, default ``True`` Let the object automatically check if the laser is controlled by setting the wavelength or diode temperature force_wl_control_available : bool, default ``False`` Force the object to assume the wavelength of the laser can be set force_temp_control_available : bool, default ``False`` Force the object to assume the temperature of the laser diode can be set """ _ip = IP _service_psw = "look in datasheet" """Custom SERVICE user level password unique to the DLCpro""" _is_open = False _remote_parameters = None _scan_parameters = None _lims = None calibration = None """MHz/mA or MHz/V calibration for the internal scan. Set by calling the ``freq_per_sec_internal_scan()`` method. After being set, the calibration will be kept in memory for future calls""" wl_control_available = False """Tells the object whether the laser is controlled with a wavelength setpoint""" temp_control_available = False """Tells the object whether the laser is controlled with a temperature setpoint""" client = None """After opening the connection the client can be used to control any setting for the DLCpro, for instance `client.set("laser1:dl:cc:current-act", 10)`""" def __init__( self, ip: Union[str, None] = None, open_on_init: bool = True, discover_wl_or_temp_control: bool = True, force_wl_control_available: bool = False, force_temp_control_available: bool = False, ): self.discover_wl_or_temp_control = discover_wl_or_temp_control if force_wl_control_available: self.wl_control_available = True self.discover_wl_or_temp_control = False if force_temp_control_available: self.temp_control_available = True self.discover_wl_or_temp_control = False if ip is not None: self._ip = ip self.connection = dlcsdk.NetworkConnection(self._ip) self.client = client.Client(self.connection) self.dlc = dlcsdk.DLCpro(self.connection) if open_on_init: self.open() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def open(self): """Open the connection to the laser and get all the parameters of the laser required to use the class""" self.dlc.open() self._is_open = True # Make sure all class attributes are up to date if self.discover_wl_or_temp_control: self._discover_control() self.get_limits_from_dlc() self.get_scan_parameters() self.get_remote_parameters() self._update_scan_range_attribute() def close(self): """Close the connection to the DLC""" if self._is_open: self.dlc.close() def set_user_level( self, level: int, password: str = "default", verbose: bool = True ): """Sets the user level privileges of the client *connection*, does not change the user level on the DLCpro console Parameters ---------- level : int User level where 3 is normal, 2 is maintenance, 1 is service password : str Password for accessing this level. Using `default` will select the correct passoword for level 3 and 2. For level 1, the password is unique to the DLCpro and can be found in the datasheet. """ if password == "default": if level == 1: print( "CAUTION: This is SERVICE level user, protected by a custom password for each unit." ) inp = input("Do you really really want to proceed? [y/N] ") if inp.lower() != "y": print("Aborting user level change") return password = self._service_psw elif level == 2: password = MAINTENANCE_PSW ul = decop.UserLevel(level) result = self.dlc.change_ul(ul, password) if verbose: print(f"New user level: {result.name}") def get_user_level(self) -> decop.UserLevel: """Gets the user level privileges of the *connection*, does not reflect the user level on the DLCpro console""" return decop.UserLevel(self.client.get("ul")) # Limits and settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## def _discover_control(self, verbose=False): # Check for wavelength control try: self.dlc.laser1.ctl.wavelength_min.get() self.wl_control_available = True except decop.DecopError as e: errormsg = e.args[0] if not ("-20" in errormsg or "unavailable" in errormsg): print( f"Unknown error '{e}' when discovering if wavelength control is available" ) self.temp_control_available = False # Check for laser diode temperature control try: self.dlc.laser1.dl.tc.temp_set_min.get() self.temp_control_available = True except decop.DecopError as e: errormsg = e.args[0] if not ("-20" in errormsg or "unavailable" in errormsg): print( f"Unknown error '{e}' when discovering if laser diode temperature control is available" ) self.temp_control_available = False if verbose: print(f"The laser has wavelength control: {self.wl_control_available}") print( f"The laser has diode temperature control: {self.temp_control_available}" ) def get_limits_from_dlc(self, verbose=False) -> dict: """Query the laser for the wavelength, piezo voltage, current and scan frequency limits, and populate the ``_lims`` dict attribute Returns ------- self._lims : dict The limits """ self._lims = { "vmin": self.dlc.laser1.dl.pc.voltage_min.get(), "vmax": self.dlc.laser1.dl.pc.voltage_max.get(), "cmin": 0.0, "cmax": self.dlc.laser1.dl.cc.current_clip.get(), "fmin": 0.02, "fmax": 400, # cannot find max in manual "tmin": None, "tmax": None, "wlmin": None, "wlmax": None, } if self.wl_control_available: self._lims.update( { "wlmin": self.dlc.laser1.ctl.wavelength_min.get(), "wlmax": self.dlc.laser1.ctl.wavelength_max.get(), } ) if self.temp_control_available: self._lims.update( { "tmin": self.dlc.laser1.dl.tc.temp_set_min.get(), "tmax": self.dlc.laser1.dl.tc.temp_set_max.get(), } ) if verbose: _print_dict(self._lims) return self._lims def get_scan_parameters(self, verbose: bool = False) -> dict: """Query the laser for the current scan settings, populate the ``_scan_parameters`` dict attribute Returns ------- self._scan_parameters : dict All parameters for the internal scan """ self._scan_parameters = { "enabled": self.scan_enabled, "output channel": self.scan_output_channel, "frequency": self.scan_frequency, "amplitude": self.scan_amplitude, "offset": self.scan_offset, "start": self.scan_start, "end": self.scan_end, } if verbose: _print_dict(self._scan_parameters) return self._scan_parameters @property def _vrange(self): return self._lims["vmin"], self._lims["vmax"] @property def _crange(self): return self._lims["cmin"], self._lims["cmax"] @property def _trange(self): return self._lims["tmin"], self._lims["tmax"] @property def _wlrange(self): return self._lims["wlmin"], self._lims["wlmax"] def _update_scan_range_attribute(self, channel: Union[None, OutputChannel] = None): if channel is None: channel = self._scan_parameters["output channel"] if channel == OutputChannel.CC: self._scan_range = self._crange elif channel == OutputChannel.PC: self._scan_range = self._vrange else: self._scan_range = [-np.inf, np.inf] print("(!) Warning: Scan range for OutA and OutB is not limted", flush=True) def get_remote_parameters(self, verbose: bool = False) -> dict: """Query the laser for the analogue remote control settings, and populate the ``_remote_parameters`` dict attribute Returns ------- self._scan_parameters : dict All parameters for the analogue remote control """ self._remote_parameters = {} for unit in ("cc", "pc"): self.remote_select = unit self._remote_parameters[unit] = { "enabled": self.remote_enabled, "factor": self.remote_factor, "signal": self.remote_signal, } if verbose: _print_dict(self._remote_parameters) return self._remote_parameters def get_all_parameters(self, verbose: bool = False) -> dict: """Returns an updated dictionary of all the parameters that can be set with the module Returns ------- dict A nested dictionary with the parameters """ timestamp = datetime.datetime.now() wls = { "wl setpoint": self.wavelength_setpoint, "wl actual": self.wavelength_actual, } temps = {"temp setpoint": self.temp_setpoint, "temp actual": self.temp_actual} # Updating scan parameters as they are interdependent params = { "timestamp": str(timestamp), "scan": self.get_scan_parameters(), "analogue remote": self._remote_parameters, "wavelength": wls, "temperature": temps, } if verbose: _print_dict(params) return params def save_parameters(self, fname: str): """Grab an updated set of laser parameters and save to a ``json`` file Raises ------ RuntimeError If a file with name `fname` already exists """ params = self.get_all_parameters() if not fname.endswith(".json"): fname += ".json" if os.path.exists(fname): raise RuntimeError(f"File '{fname}' already exists") with open(fname, "w") as outfile: json.dump(params, outfile, indent=" ") @staticmethod def read_parameters(fname: str, verbose: bool = True) -> dict: """Read (but not set!) parameters from json file""" if not fname.endswith(".json"): fname += ".json" with open(fname) as json_file: params = json.load(json_file) if verbose: _print_dict(params) return params def set_parameters(self, params: dict): """*Not yet implemented* The idea is to be able to use the parameters in a dict and set them accordingly""" raise NotImplementedError("Still to be implemented") def verbose_emission_status(self): """Print the emission status of the laser, for example ``` Emission button is ENABLED Laser current is DISABLED Therefore, emission is ON ``` """ print(f"Emission button is {_ENABLED_DISABLED[self.emission_button]}") print(f"Laser current is {_ENABLED_DISABLED[self.current_enabled]}") print(f"Therefore, emission is {_ON_OFF[self.emission]}") def freq_per_sec_internal_scan(self, calibration: float = None) -> float: """Calculate frequency span per second for the laser in MHz per second from the scan parameters Parameters ---------- calibration : float [MHz/mA or MHz/V] """ params = self.get_scan_parameters() scan_freq = params["frequency"] peak_to_peak = params["amplitude"] if calibration is not None: self.calibration = calibration return freq_per_sec( scan_freq, peak_to_peak, scaling=1, calibration=self.calibration ) # Emission properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @property def emission(self) -> bool: """Emission status of the DLC (read only)""" return self.dlc.emission.get() @property def emission_button(self) -> bool: """Status of the emission button of the DLC (read only)""" return self.dlc.emission_button_enabled.get() @property def current_enabled(self) -> bool: """Status of the current to the laser""" return self.dlc.laser1.dl.cc.enabled.get() @current_enabled.setter def current_enabled(self, val: bool): """Sneaky way to control emission on/off provided the button on the DLC is enabled""" if val and not self.emission_button: print("(!) Emission button on DLC not enabled, so cannot enable emission") self.dlc.laser1.dl.cc.enabled.set(val) # Wavelength properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @property def wavelength_actual(self) -> float: """The actual wavelength of the laser (read only)""" if not self.wl_control_available: return None return self.dlc.laser1.ctl.wavelength_act.get() @property def wavelength_setpoint(self) -> float: """The setpont of the laser wavelength""" if not self.wl_control_available: return None return self.dlc.laser1.ctl.wavelength_set.get() @wavelength_setpoint.setter def wavelength_setpoint(self, val: float): if not self.wl_control_available: raise RuntimeError( "Cannot set wavelength when `wl_control_available` is False" ) if val is None: return val = float(val) if self._wlrange[0] is None: self.get_limits_from_dlc() _check_value(val, "wavelength setpoint", self._wlrange) self.dlc.laser1.ctl.wavelength_set.set(val) ## Temperature properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @property def temp_actual(self) -> float: """The actual temperature of the laser diode (read only)""" if not self.temp_control_available: return None return self.dlc.laser1.dl.tc.temp_act.get() @property def temp_setpoint(self) -> float: """The setpoint of the laser diode temperature""" if not self.temp_control_available: return None return self.dlc.laser1.dl.tc.temp_set.get() @temp_setpoint.setter def temp_setpoint(self, val: float): if not self.temp_control_available: raise RuntimeError( "Cannot set diode temperature `temp_control_available` is False" ) if val is None: return val = float(val) if self._trange[0] is None: self.get_limits_from_dlc() _check_value(val, "temperature setpoint", self._trange) self.dlc.laser1.dl.tc.temp_set.set(val) # Remote properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @property def remote_select(self) -> Tuple[str, Any]: """Analogue Remote Control for both the DLCpro's current (cc) and voltage (pc) can be used simultaneously. With this class, both can be used simultaneously, with this select property choosing which remote is receiveing the commands at any given time Example ------- with DLCcontrol(ip) as dlc: # Choose to set the ARC for the current dlc.remote_select = "CC" # Decide its input.. dlc.remote_signal = "Fine1" # ..and enable it dlc.remote_enable = True # Now move to the ARC for the piezo.. dlc.remote_select = "PC" # ..and choose some settings for it dlc.remote_signal = "Fast3" dlc.remote_enable = True """ return self._remote_str, self._remote_unit @remote_select.setter def remote_select(self, select: str): """ select : {"pc", "cc"} """ if select.lower() == "cc": self._remote_str = "cc" self._remote_unit = self.dlc.laser1.dl.cc.external_input elif select.lower() == "pc": self._remote_str = "pc" self._remote_unit = self.dlc.laser1.dl.pc.external_input else: raise ValueError( f"select must be either 'pc' nor 'cc' (tried using '{select}')" ) @property def remote_enabled(self) -> bool: """Status of the chosen remote""" return self._remote_unit.enabled.get() @remote_enabled.setter def remote_enabled(self, val: bool): self._remote_unit.enabled.set(val) self._remote_parameters[self._remote_str]["enabled"] = val @property def remote_signal(self) -> InputChannel: """The input port the chosen remote uses""" num = self._remote_unit.signal.get() return InputChannel(num) @remote_signal.setter def remote_signal(self, val: Union[InputChannel, str]): """Choose which output channel to use for the ARC val : {"Fine1", "Fine2", "Fast3", "Fast4", InputChannel.Fine1, InputChannel.Fine2, InputChannel.Fast3, InputChannel.Fast4}""" try: if isinstance(val, InputChannel): num = val.value elif isinstance(val, str): num = InputChannel[val.title()].value else: raise KeyError except KeyError: raise ValueError( "Input channel must be one of 'Fine1', 'Fine2', " f"'Fast3', 'Fast4', or an InputChannel (tried with '{val}')" ) from KeyError self._remote_unit.signal.set(num) self._remote_parameters[self._remote_str]["signal"] = InputChannel(num) @property def remote_factor(self) -> float: """The numerical factor the remote signal is multiplied with before used as the current or piezo control""" return self._remote_unit.factor.get() @remote_factor.setter def remote_factor(self, val: float): val = float(val) self._remote_unit.factor.set(val) self._remote_parameters[self._remote_str]["factor"] = val # Scan properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## @property def scan_enabled(self) -> bool: """Internal scan on/off""" return self.dlc.laser1.scan.enabled.get() @scan_enabled.setter def scan_enabled(self, val: bool): self.dlc.laser1.scan.enabled.set(val) self._scan_parameters["enabled"] = val @property def scan_output_channel(self) -> OutputChannel: """Internal scan output channel. It can be directed to the piezo or laser current directly, or to the output BNCs on the DLC""" num = self.dlc.laser1.scan.output_channel.get() return OutputChannel(num) @scan_output_channel.setter def scan_output_channel(self, val: Union[OutputChannel, str]): """The internal scan can only act on eiter piezo or the current at any given time, or be directed to the DLC BNCs val : {"CC", "PC", OutputChannel.CC, OutputChannel.PC}""" try: if isinstance(val, OutputChannel): num = val.value elif isinstance(val, str): num = OutputChannel[val.upper()].value else: raise KeyError except KeyError: raise ValueError( "Channel must be 'CC', 'PC', OutputChannel.CC, or " f"OutputChannel.PC (tried with '{val}')" ) from KeyError self.dlc.laser1.scan.output_channel.set(num) self._scan_parameters["scan_output_channel"] = OutputChannel(num) self._update_scan_range_attribute(OutputChannel(num)) @property def scan_frequency(self) -> float: """Internal scan frequency""" return self.dlc.laser1.scan.frequency.get() @scan_frequency.setter def scan_frequency(self, val: float): val = float(val) _check_value(val, "scan frequency", (self._lims["fmin"], self._lims["fmax"])) self.dlc.laser1.scan.frequency.set(val) self._scan_parameters["frequency"] = val @property def scan_amplitude(self) -> float: """Internal scan amplitude""" return self.dlc.laser1.scan.amplitude.get() @scan_amplitude.setter def scan_amplitude(self, val: float): val = float(val) offset = self.scan_offset new_range = [offset - val / 2, offset + val / 2] if min(new_range) < self._scan_range[0] or max(new_range) > self._scan_range[1]: raise OutOfRangeError(new_range, "scan", self._scan_range) self.dlc.laser1.scan.amplitude.set(val) self._scan_parameters["amplitude"] = val @property def scan_offset(self) -> float: """Internal scan offset value""" return self.dlc.laser1.scan.offset.get() @scan_offset.setter def scan_offset(self, val: float): val = float(val) amplitude = self.scan_amplitude new_range = [val - amplitude / 2, val + amplitude / 2] if min(new_range) < self._scan_range[0] or max(new_range) > self._scan_range[1]: raise OutOfRangeError(new_range, "scan", self._scan_range) self.dlc.laser1.scan.offset.set(val) self._scan_parameters["offset"] = val @property def scan_start(self) -> float: """Internal scan start value""" return self.dlc.laser1.scan.start.get() @scan_start.setter def scan_start(self, val: float): val = float(val) _check_value(val, "scan start", self._scan_range) self.dlc.laser1.scan.start.set(val) self._scan_parameters["start"] = val @property def scan_end(self) -> float: """Interal scan end value""" return self.dlc.laser1.scan.end.get() @scan_end.setter def scan_end(self, val: float): val = float(val) _check_value(val, "scan end", self._scan_range) self.dlc.laser1.scan.end.set(val) self._scan_parameters["end"] = valClass variables- var calibration
- 
MHz/mA or MHz/V calibration for the internal scan. Set by calling the freq_per_sec_internal_scan()method. After being set, the calibration will be kept in memory for future calls
- var client
- 
After opening the connection the client can be used to control any setting for the DLCpro, for instance client.set("laser1:dl:cc:current-act", 10)
- var temp_control_available
- 
Tells the object whether the laser is controlled with a temperature setpoint 
- var wl_control_available
- 
Tells the object whether the laser is controlled with a wavelength setpoint 
 Static methods- def read_parameters(fname: str, verbose: bool = True) ‑> dict
- 
Read (but not set!) parameters from json file Expand source code@staticmethod def read_parameters(fname: str, verbose: bool = True) -> dict: """Read (but not set!) parameters from json file""" if not fname.endswith(".json"): fname += ".json" with open(fname) as json_file: params = json.load(json_file) if verbose: _print_dict(params) return params
 Instance variables- var current_enabled : bool
- 
Status of the current to the laser Expand source code@property def current_enabled(self) -> bool: """Status of the current to the laser""" return self.dlc.laser1.dl.cc.enabled.get()
- var emission : bool
- 
Emission status of the DLC (read only) Expand source code@property def emission(self) -> bool: """Emission status of the DLC (read only)""" return self.dlc.emission.get()
- 
Status of the emission button of the DLC (read only) Expand source code@property def emission_button(self) -> bool: """Status of the emission button of the DLC (read only)""" return self.dlc.emission_button_enabled.get()
- var remote_enabled : bool
- 
Status of the chosen remote Expand source code@property def remote_enabled(self) -> bool: """Status of the chosen remote""" return self._remote_unit.enabled.get()
- var remote_factor : float
- 
The numerical factor the remote signal is multiplied with before used as the current or piezo control Expand source code@property def remote_factor(self) -> float: """The numerical factor the remote signal is multiplied with before used as the current or piezo control""" return self._remote_unit.factor.get()
- var remote_select : Tuple[str, Any]
- 
Analogue Remote Control for both the DLCpro's current (cc) and voltage (pc) can be used simultaneously. With this class, both can be used simultaneously, with this select property choosing which remote is receiveing the commands at any given time Examplewith DLCcontrol(ip) as dlc: # Choose to set the ARC for the current dlc.remote_select = "CC" # Decide its input.. dlc.remote_signal = "Fine1" # ..and enable it dlc.remote_enable = True # Now move to the ARC for the piezo.. dlc.remote_select = "PC" # ..and choose some settings for it dlc.remote_signal = "Fast3" dlc.remote_enable = TrueExpand source code@property def remote_select(self) -> Tuple[str, Any]: """Analogue Remote Control for both the DLCpro's current (cc) and voltage (pc) can be used simultaneously. With this class, both can be used simultaneously, with this select property choosing which remote is receiveing the commands at any given time Example ------- with DLCcontrol(ip) as dlc: # Choose to set the ARC for the current dlc.remote_select = "CC" # Decide its input.. dlc.remote_signal = "Fine1" # ..and enable it dlc.remote_enable = True # Now move to the ARC for the piezo.. dlc.remote_select = "PC" # ..and choose some settings for it dlc.remote_signal = "Fast3" dlc.remote_enable = True """ return self._remote_str, self._remote_unit
- var remote_signal : InputChannel
- 
The input port the chosen remote uses Expand source code@property def remote_signal(self) -> InputChannel: """The input port the chosen remote uses""" num = self._remote_unit.signal.get() return InputChannel(num)
- var scan_amplitude : float
- 
Internal scan amplitude Expand source code@property def scan_amplitude(self) -> float: """Internal scan amplitude""" return self.dlc.laser1.scan.amplitude.get()
- var scan_enabled : bool
- 
Internal scan on/off Expand source code@property def scan_enabled(self) -> bool: """Internal scan on/off""" return self.dlc.laser1.scan.enabled.get()
- var scan_end : float
- 
Interal scan end value Expand source code@property def scan_end(self) -> float: """Interal scan end value""" return self.dlc.laser1.scan.end.get()
- var scan_frequency : float
- 
Internal scan frequency Expand source code@property def scan_frequency(self) -> float: """Internal scan frequency""" return self.dlc.laser1.scan.frequency.get()
- var scan_offset : float
- 
Internal scan offset value Expand source code@property def scan_offset(self) -> float: """Internal scan offset value""" return self.dlc.laser1.scan.offset.get()
- var scan_output_channel : OutputChannel
- 
Internal scan output channel. It can be directed to the piezo or laser current directly, or to the output BNCs on the DLC Expand source code@property def scan_output_channel(self) -> OutputChannel: """Internal scan output channel. It can be directed to the piezo or laser current directly, or to the output BNCs on the DLC""" num = self.dlc.laser1.scan.output_channel.get() return OutputChannel(num)
- var scan_start : float
- 
Internal scan start value Expand source code@property def scan_start(self) -> float: """Internal scan start value""" return self.dlc.laser1.scan.start.get()
- var temp_actual : float
- 
The actual temperature of the laser diode (read only) Expand source code@property def temp_actual(self) -> float: """The actual temperature of the laser diode (read only)""" if not self.temp_control_available: return None return self.dlc.laser1.dl.tc.temp_act.get()
- var temp_setpoint : float
- 
The setpoint of the laser diode temperature Expand source code@property def temp_setpoint(self) -> float: """The setpoint of the laser diode temperature""" if not self.temp_control_available: return None return self.dlc.laser1.dl.tc.temp_set.get()
- var wavelength_actual : float
- 
The actual wavelength of the laser (read only) Expand source code@property def wavelength_actual(self) -> float: """The actual wavelength of the laser (read only)""" if not self.wl_control_available: return None return self.dlc.laser1.ctl.wavelength_act.get()
- var wavelength_setpoint : float
- 
The setpont of the laser wavelength Expand source code@property def wavelength_setpoint(self) -> float: """The setpont of the laser wavelength""" if not self.wl_control_available: return None return self.dlc.laser1.ctl.wavelength_set.get()
 Methods- def close(self)
- 
Close the connection to the DLC Expand source codedef close(self): """Close the connection to the DLC""" if self._is_open: self.dlc.close()
- def freq_per_sec_internal_scan(self, calibration: float = None) ‑> float
- 
Calculate frequency span per second for the laser in MHz per second from the scan parameters Parameters- calibration:- float
- [MHz/mA or MHz/V]
 Expand source codedef freq_per_sec_internal_scan(self, calibration: float = None) -> float: """Calculate frequency span per second for the laser in MHz per second from the scan parameters Parameters ---------- calibration : float [MHz/mA or MHz/V] """ params = self.get_scan_parameters() scan_freq = params["frequency"] peak_to_peak = params["amplitude"] if calibration is not None: self.calibration = calibration return freq_per_sec( scan_freq, peak_to_peak, scaling=1, calibration=self.calibration )
- def get_all_parameters(self, verbose: bool = False) ‑> dict
- 
Returns an updated dictionary of all the parameters that can be set with the module Returns- dict
- A nested dictionary with the parameters
 Expand source codedef get_all_parameters(self, verbose: bool = False) -> dict: """Returns an updated dictionary of all the parameters that can be set with the module Returns ------- dict A nested dictionary with the parameters """ timestamp = datetime.datetime.now() wls = { "wl setpoint": self.wavelength_setpoint, "wl actual": self.wavelength_actual, } temps = {"temp setpoint": self.temp_setpoint, "temp actual": self.temp_actual} # Updating scan parameters as they are interdependent params = { "timestamp": str(timestamp), "scan": self.get_scan_parameters(), "analogue remote": self._remote_parameters, "wavelength": wls, "temperature": temps, } if verbose: _print_dict(params) return params
- def get_limits_from_dlc(self, verbose=False) ‑> dict
- 
Query the laser for the wavelength, piezo voltage, current and scan frequency limits, and populate the _limsdict attributeReturns- self._lims : dict
- The limits
 Expand source codedef get_limits_from_dlc(self, verbose=False) -> dict: """Query the laser for the wavelength, piezo voltage, current and scan frequency limits, and populate the ``_lims`` dict attribute Returns ------- self._lims : dict The limits """ self._lims = { "vmin": self.dlc.laser1.dl.pc.voltage_min.get(), "vmax": self.dlc.laser1.dl.pc.voltage_max.get(), "cmin": 0.0, "cmax": self.dlc.laser1.dl.cc.current_clip.get(), "fmin": 0.02, "fmax": 400, # cannot find max in manual "tmin": None, "tmax": None, "wlmin": None, "wlmax": None, } if self.wl_control_available: self._lims.update( { "wlmin": self.dlc.laser1.ctl.wavelength_min.get(), "wlmax": self.dlc.laser1.ctl.wavelength_max.get(), } ) if self.temp_control_available: self._lims.update( { "tmin": self.dlc.laser1.dl.tc.temp_set_min.get(), "tmax": self.dlc.laser1.dl.tc.temp_set_max.get(), } ) if verbose: _print_dict(self._lims) return self._lims
- def get_remote_parameters(self, verbose: bool = False) ‑> dict
- 
Query the laser for the analogue remote control settings, and populate the _remote_parametersdict attributeReturns- self._scan_parameters : dict
- All parameters for the analogue remote control
 Expand source codedef get_remote_parameters(self, verbose: bool = False) -> dict: """Query the laser for the analogue remote control settings, and populate the ``_remote_parameters`` dict attribute Returns ------- self._scan_parameters : dict All parameters for the analogue remote control """ self._remote_parameters = {} for unit in ("cc", "pc"): self.remote_select = unit self._remote_parameters[unit] = { "enabled": self.remote_enabled, "factor": self.remote_factor, "signal": self.remote_signal, } if verbose: _print_dict(self._remote_parameters) return self._remote_parameters
- def get_scan_parameters(self, verbose: bool = False) ‑> dict
- 
Query the laser for the current scan settings, populate the _scan_parametersdict attributeReturns- self._scan_parameters : dict
- All parameters for the internal scan
 Expand source codedef get_scan_parameters(self, verbose: bool = False) -> dict: """Query the laser for the current scan settings, populate the ``_scan_parameters`` dict attribute Returns ------- self._scan_parameters : dict All parameters for the internal scan """ self._scan_parameters = { "enabled": self.scan_enabled, "output channel": self.scan_output_channel, "frequency": self.scan_frequency, "amplitude": self.scan_amplitude, "offset": self.scan_offset, "start": self.scan_start, "end": self.scan_end, } if verbose: _print_dict(self._scan_parameters) return self._scan_parameters
- def get_user_level(self) ‑> toptica.lasersdk.decop.UserLevel
- 
Gets the user level privileges of the connection, does not reflect the user level on the DLCpro console Expand source codedef get_user_level(self) -> decop.UserLevel: """Gets the user level privileges of the *connection*, does not reflect the user level on the DLCpro console""" return decop.UserLevel(self.client.get("ul"))
- def open(self)
- 
Open the connection to the laser and get all the parameters of the laser required to use the class Expand source codedef open(self): """Open the connection to the laser and get all the parameters of the laser required to use the class""" self.dlc.open() self._is_open = True # Make sure all class attributes are up to date if self.discover_wl_or_temp_control: self._discover_control() self.get_limits_from_dlc() self.get_scan_parameters() self.get_remote_parameters() self._update_scan_range_attribute()
- def save_parameters(self, fname: str)
- 
Grab an updated set of laser parameters and save to a jsonfileRaises- RuntimeError
- If a file with name fnamealready exists
 Expand source codedef save_parameters(self, fname: str): """Grab an updated set of laser parameters and save to a ``json`` file Raises ------ RuntimeError If a file with name `fname` already exists """ params = self.get_all_parameters() if not fname.endswith(".json"): fname += ".json" if os.path.exists(fname): raise RuntimeError(f"File '{fname}' already exists") with open(fname, "w") as outfile: json.dump(params, outfile, indent=" ")
- def set_parameters(self, params: dict)
- 
Not yet implemented The idea is to be able to use the parameters in a dict and set them accordingly Expand source codedef set_parameters(self, params: dict): """*Not yet implemented* The idea is to be able to use the parameters in a dict and set them accordingly""" raise NotImplementedError("Still to be implemented")
- def set_user_level(self, level: int, password: str = 'default', verbose: bool = True)
- 
Sets the user level privileges of the client connection, does not change the user level on the DLCpro console Parameters- level:- int
- User level where 3 is normal, 2 is maintenance, 1 is service
- password:- str
- Password for accessing this level. Using defaultwill select the correct passoword for level 3 and 2. For level 1, the password is unique to the DLCpro and can be found in the datasheet.
 Expand source codedef set_user_level( self, level: int, password: str = "default", verbose: bool = True ): """Sets the user level privileges of the client *connection*, does not change the user level on the DLCpro console Parameters ---------- level : int User level where 3 is normal, 2 is maintenance, 1 is service password : str Password for accessing this level. Using `default` will select the correct passoword for level 3 and 2. For level 1, the password is unique to the DLCpro and can be found in the datasheet. """ if password == "default": if level == 1: print( "CAUTION: This is SERVICE level user, protected by a custom password for each unit." ) inp = input("Do you really really want to proceed? [y/N] ") if inp.lower() != "y": print("Aborting user level change") return password = self._service_psw elif level == 2: password = MAINTENANCE_PSW ul = decop.UserLevel(level) result = self.dlc.change_ul(ul, password) if verbose: print(f"New user level: {result.name}")
- def verbose_emission_status(self)
- 
Print the emission status of the laser, for example Emission button is ENABLED Laser current is DISABLED Therefore, emission is ONExpand source codedef verbose_emission_status(self): """Print the emission status of the laser, for example ``` Emission button is ENABLED Laser current is DISABLED Therefore, emission is ON ``` """ print(f"Emission button is {_ENABLED_DISABLED[self.emission_button]}") print(f"Laser current is {_ENABLED_DISABLED[self.current_enabled]}") print(f"Therefore, emission is {_ON_OFF[self.emission]}")
 
- class InputChannel (value, names=None, *, module=None, qualname=None, type=None, start=1)
- 
Input channel name to numeric value conversion Expand source codeclass InputChannel(int, enum.Enum): """Input channel name to numeric value conversion""" NotSelected = -3 Fine1 = 0 Fine2 = 1 Fast3 = 2 Fast4 = 3Ancestors- builtins.int
- enum.Enum
 Class variables- var Fast3
- var Fast4
- var Fine1
- var Fine2
- var NotSelected
 
- class OutOfRangeError (value: Any, parameter_name: str, permitted_range: List[Any])
- 
Custom out of range errors for when a parameter is outside the permitted range Expand source codeclass OutOfRangeError(ValueError): """Custom out of range errors for when a parameter is outside the permitted range""" def __init__(self, value: Any, parameter_name: str, permitted_range: List[Any]): self.value = value self.parameter_name = parameter_name self.range = permitted_range self.message = ( f"{value} is not within the permitted " f"{parameter_name} range {permitted_range}" ) super().__init__(self.message)Ancestors- builtins.ValueError
- builtins.Exception
- builtins.BaseException
 
- class OutputChannel (value, names=None, *, module=None, qualname=None, type=None, start=1)
- 
Output channel name to numeric value conversion Expand source codeclass OutputChannel(int, enum.Enum): # int needed to avoid custom json serialiser """Output channel name to numeric value conversion""" PC = 50 CC = 51 OutA = 20 OutB = 21Ancestors- builtins.int
- enum.Enum
 Class variables- var CC
- var OutA
- var OutB
- var PC