Source code for instruments.thorlabs.tc200

#!/usr/bin/env python
"""
Provides the support for the Thorlabs TC200 temperature controller.

Class originally contributed by Catherine Holloway.
"""

# IMPORTS #####################################################################

from enum import IntEnum, Enum

from instruments.abstract_instruments import Instrument
from instruments.units import ureg as u
from instruments.util_fns import (
    convert_temperature,
    enum_property,
    unitful_property,
    int_property,
)

# CLASSES #####################################################################


[docs] class TC200(Instrument): """ The TC200 is is a controller for the voltage across a heating element. It can also read in the temperature off of a thermistor and implements a PID control to keep the temperature at a set value. The user manual can be found here: http://www.thorlabs.com/thorcat/12500/TC200-Manual.pdf """ def __init__(self, filelike): super().__init__(filelike) self.terminator = "\r" self.prompt = "> " def _ack_expected(self, msg=""): return msg # ENUMS #
[docs] class Mode(IntEnum): """ Enum containing valid output modes of the TC200. """ normal = 0 cycle = 1
[docs] class Sensor(Enum): """ Enum containing valid temperature sensor types for the TC200. """ ptc100 = "ptc100" ptc1000 = "ptc1000" th10k = "th10k" ntc10k = "ntc10k"
# PROPERTIES #
[docs] def name(self): """ Gets the name and version number of the device :return: the name string of the device :rtype: str """ response = self.query("*idn?") return response
@property def mode(self): """ Gets/sets the output mode of the TC200 :type: `TC200.Mode` """ response = self.status response_code = (int(response) >> 1) % 2 return TC200.Mode(response_code) @mode.setter def mode(self, newval): if not isinstance(newval, TC200.Mode): raise TypeError( "Mode setting must be a `TC200.Mode` value, " "got {} instead.".format(type(newval)) ) out_query = f"mode={newval.name}" # there is an issue with the TC200; it responds with a spurious # Command Error on mode=normal. Thus, the sendcmd() method cannot # be used. if newval == TC200.Mode.normal: self.prompt = "Command error CMD_ARG_RANGE_ERR\n\r> " self.sendcmd(out_query) self.prompt = "> " else: self.sendcmd(out_query) @property def enable(self): """ Gets/sets the heater enable status. If output enable is on (`True`), there is a voltage on the output. :type: `bool` """ response = self.status return True if int(response) % 2 == 1 else False @enable.setter def enable(self, newval): if not isinstance(newval, bool): raise TypeError( "TC200 enable property must be specified with a " "boolean." ) # the "ens" command is a toggle, we need to track two different cases, # when it should be on and it is off, and when it is off and # should be on # if no sensor is attached, the unit will respond with an error. # There is no current error handling in the way that thorlabs # responds with errors if newval and not self.enable: response1 = self._file.query("ens") while response1 != ">": response1 = self._file.read(1) self._file.read(1) elif not newval and self.enable: response1 = self._file.query("ens") while response1 != ">": response1 = self._file.read(1) self._file.read(1) @property def status(self): """ Gets the the status code of the TC200 :rtype: `int` """ _ = self._file.query("stat?") response = self.read(5) return int(response.split(" ")[0]) temperature = unitful_property( "tact", units=u.degC, readonly=True, input_decoration=lambda x: x.replace(" C", "") .replace(" F", "") .replace(" K", ""), doc=""" Gets the actual temperature of the sensor :units: As specified (if a `~pint.Quantity`) or assumed to be of units degrees C. :type: `~pint.Quantity` or `int` :return: the temperature (in degrees C) :rtype: `~pint.Quantity` """, ) max_temperature = unitful_property( "tmax", units=u.degC, format_code="{:.1f}", set_fmt="{}={}", valid_range=(u.Quantity(20, u.degC), u.Quantity(205, u.degC)), doc=""" Gets/sets the maximum temperature :return: the maximum temperature (in deg C) :units: As specified or assumed to be degree Celsius. Returns with units degC. :rtype: `~pint.Quantity` """, ) @property def temperature_set(self): """ Gets/sets the actual temperature of the sensor :units: As specified (if a `~pint.Quantity`) or assumed to be of units degrees C. :type: `~pint.Quantity` or `int` :return: the temperature (in degrees C) :rtype: `~pint.Quantity` """ response = ( self.query("tset?") .replace(" Celsius", "") .replace(" C", "") .replace(" F", "") .replace(" K", "") ) return u.Quantity(float(response), u.degC) @temperature_set.setter def temperature_set(self, newval): # the set temperature is always in celsius newval = convert_temperature(newval, u.degC) if newval < u.Quantity(20.0, u.degC) or newval > self.max_temperature: raise ValueError("Temperature set is out of range.") out_query = f"tset={newval.magnitude}" self.sendcmd(out_query) @property def p(self): """ Gets/sets the p-gain. Valid numbers are [1,250]. :return: the p-gain (in nnn) :rtype: `int` """ return self.pid[0] @p.setter def p(self, newval): if newval not in range(1, 251): raise ValueError("P-value not in [1, 250]") self.sendcmd(f"pgain={newval}") @property def i(self): """ Gets/sets the i-gain. Valid numbers are [1,250] :return: the i-gain (in nnn) :rtype: `int` """ return self.pid[1] @i.setter def i(self, newval): if newval not in range(0, 251): raise ValueError("I-value not in [0, 250]") self.sendcmd(f"igain={newval}") @property def d(self): """ Gets/sets the d-gain. Valid numbers are [0, 250] :return: the d-gain (in nnn) :type: `int` """ return self.pid[2] @d.setter def d(self, newval): if newval not in range(0, 251): raise ValueError("D-value not in [0, 250]") self.sendcmd(f"dgain={newval}") @property def pid(self): """ Gets/sets all three PID values at the same time. See `TC200.p`, `TC200.i`, and `TC200.d` for individual restrictions. If `None` is specified then the corresponding PID value is not changed. :return: List of integers of PID values. In order [P, I, D]. :type: `list` or `tuple` :rtype: `list` """ return list(map(int, self.query("pid?").split())) @pid.setter def pid(self, newval): if not isinstance(newval, (list, tuple)): raise TypeError("Setting PID must be specified as a list or tuple") if newval[0] is not None: self.p = newval[0] if newval[1] is not None: self.i = newval[1] if newval[2] is not None: self.d = newval[2] @property def degrees(self): """ Gets/sets the units of the temperature measurement. :return: The temperature units (degC/F/K) the TC200 is measuring in :type: `~pint.Unit` """ response = self.status if (response >> 4) % 2 and (response >> 5) % 2: return u.degC elif (response >> 5) % 2: return u.degK return u.degF @degrees.setter def degrees(self, newval): if newval == u.degC: self.sendcmd("unit=c") elif newval == u.degF: self.sendcmd("unit=f") elif newval == u.degK: self.sendcmd("unit=k") else: raise TypeError("Invalid temperature type") sensor = enum_property( "sns", Sensor, input_decoration=lambda x: x.split(",")[0].split("=")[1].strip().lower(), set_fmt="{}={}", doc=""" Gets/sets the current thermistor type. Used for converting resistances to temperatures. :return: The thermistor type :type: `TC200.Sensor` """, ) beta = int_property( "beta", valid_set=range(2000, 6001), set_fmt="{}={}", doc=""" Gets/sets the beta value of the thermistor curve. Value within [2000, 6000] :return: the gain (in nnn) :type: `int` """, ) max_power = unitful_property( "pmax", units=u.W, format_code="{:.1f}", set_fmt="{}={}", valid_range=(0.1 * u.W, 18.0 * u.W), doc=""" Gets/sets the maximum power :return: The maximum power :units: Watts (linear units) :type: `~pint.Quantity` """, )