Module dlccontrol

Toptica DLCpro control

CodeFactor Grade MIT License

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.client attribute

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 OutA and OutB (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 client attribute to the DLCcontrol class 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 -h to see the options

Expand source code
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)
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 code
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]
Expand source code
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)
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 DLCcontrol object is provided, a new one will not be created
Expand source code
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()

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 code
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

Class 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()
var emission_button : bool

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

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
Expand 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_signalInputChannel

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_channelOutputChannel

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 code
def 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 code
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
    )
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 code
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 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
Expand source code
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_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
Expand source code
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_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
Expand source code
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
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 code
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"))
def open(self)

Open the connection to the laser and get all the parameters of the laser required to use the class

Expand source code
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 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
Expand source code
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="  ")
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 code
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 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.
Expand source code
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 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
Expand source code
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]}")
class InputChannel (value, names=None, *, module=None, qualname=None, type=None, start=1)

Input channel name to numeric value conversion

Expand source code
class InputChannel(int, enum.Enum):
    """Input channel name to numeric value conversion"""

    NotSelected = -3
    Fine1 = 0
    Fine2 = 1
    Fast3 = 2
    Fast4 = 3

Ancestors

  • 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 code
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)

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 code
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

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var CC
var OutA
var OutB
var PC