Source code for instruments.thorlabs.tc200

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Provides the support for the Thorlabs TC200 temperature controller.

Class originally contributed by Catherine Holloway.
"""

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

from __future__ import absolute_import
from __future__ import division

from builtins import range, map
from enum import IntEnum, Enum

import quantities as pq

from instruments.abstract_instruments import Instrument
from instruments.util_fns import convert_temperature
from instruments.util_fns import 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(TC200, self).__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 = "mode={}".format(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(str("stat?")) response = self.read(5) return int(response.split(" ")[0]) temperature = unitful_property( "tact", units=pq.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 `~quantities.quantity.Quantity`) or assumed to be of units degrees C. :type: `~quantities.quantity.Quantity` or `int` :return: the temperature (in degrees C) :rtype: `~quantities.quantity.Quantity` """ ) max_temperature = unitful_property( "tmax", units=pq.degC, format_code="{:.1f}", set_fmt="{}={}", valid_range=(20*pq.degC, 205*pq.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: `~quantities.quantity.Quantity` """ ) @property def temperature_set(self): """ Gets/sets the actual temperature of the sensor :units: As specified (if a `~quantities.quantity.Quantity`) or assumed to be of units degrees C. :type: `~quantities.quantity.Quantity` or `int` :return: the temperature (in degrees C) :rtype: `~quantities.quantity.Quantity` """ response = self.query("tset?").replace( " C", "").replace(" F", "").replace(" K", "") return float(response) * pq.degC @temperature_set.setter def temperature_set(self, newval): # the set temperature is always in celsius newval = convert_temperature(newval, pq.degC).magnitude if newval < 20.0 or newval > self.max_temperature: raise ValueError("Temperature set is out of range.") out_query = "tset={}".format(newval) 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("pgain={}".format(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("igain={}".format(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("dgain={}".format(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: `~quantities.unitquantity.UnitTemperature` """ response = self.status if (response >> 4) % 2 and (response >> 5) % 2: return pq.degC elif (response >> 5) % 2: return pq.degK return pq.degF @degrees.setter def degrees(self, newval): if newval is pq.degC: self.sendcmd("unit=c") elif newval is pq.degF: self.sendcmd("unit=f") elif newval is pq.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=pq.W, format_code="{:.1f}", set_fmt="{}={}", valid_range=(0.1*pq.W, 18.0*pq.W), doc=""" Gets/sets the maximum power :return: The maximum power :units: Watts (linear units) :type: `~quantities.quantity.Quantity` """ )