Source code for instruments.sunpower.cryotel_gt

#!/usr/bin/env python
"""
Driver for the Sunpower CryoTel GT generation 2 cryocooler.
"""

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

from collections import OrderedDict
from enum import Enum
import warnings

from instruments.abstract_instruments import Instrument
from instruments.units import ureg as u
from instruments.util_fns import assume_units

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


[docs] class CryoTelGT(Instrument): """ The Sunpower CyroTel GT is a cryocooler. This driver is for the GT generation 2. According to the Sunpower website, this means for cryocoolers purchased after May 2012. Caution: Do not use this driver to established a closed loop control of the cryocooler, as this may cause malfunction and potentially damage to the device (see the manual for details). You can use this driver however to adjust the setpoint temperature and read the current temperature. For communications, the default baudrate is 4800, 8 data bits, 1 stop bit, and no flow control. Example usage: >>> import instruments as ik >>> inst = ik.sunpower.CryoTelGT.open_serial("/dev/ttyACM0", 4800) >>> inst.temperature 82.0 Kelvin >>> inst.temperature_setpoint 77.0 Kelvin """
[docs] class ControlMode(Enum): """ Control modes for the Cryocooler. """ POWER = 0 TEMPERATURE = 2
[docs] class ThermostatStatus(Enum): """ Thermostat status for the CryoTel GT. Off means that the thermostat is open and the cryocooler is shutting down or shut down. """ OFF = 0 ON = 1
[docs] class StopMode(Enum): """ Stop mode for the cryocooler. `HOST` means that the start/stop command can be controlled from the host computer. `DIGIO` means that the start/stop command can be set from the digital input/output pin 1 on the cryocooler. """ HOST = 0 DIGIO = 1
def __init__(self, filelike): super().__init__(filelike) self._error_codes = OrderedDict( { 1: "Over Current", 2: "Jumper Error", 4: "Serial Error", 8: "Non-volatile Memory Error", 16: "Watchdog Error", 32: "Temperature Sensor Error", } ) self.terminator = "\r" @property def at_temperature_band(self): """ Get/set the temperature band of the CryoTel GT in Kelvin. Returns the temperature band within the green LED and "At Temperature" pin on the I/O connector will be activated. If no unit is provided, Kelvin are assumed. :return: The current temperature band in Kelvin. """ ret_val = self.query("SET TBAND") return float(ret_val) * u.K @at_temperature_band.setter def at_temperature_band(self, value): value = assume_units(value, u.K).to(u.K) self.query("SET TBAND", float(value.magnitude)) @property def control_mode(self): """ Get/set the control mode of the CryoTel GT. Valid options are `ControlMode.POWER` and `ControlMode.TEMPERATURE`. .. note:: The set control mode will be reset after a power cycle unless you also call the `save_control_mode()` method. :return: The current control mode. """ ret_val = int(float(self.query("SET PID"))) return self.ControlMode(ret_val) @control_mode.setter def control_mode(self, value): if not isinstance(value, self.ControlMode): raise ValueError( "Invalid control mode. Use ControlMode.POWER or ControlMode.TEMPERATURE." ) self.query("SET PID", value.value) @property def errors(self): """Get any error codes from the CryoTel GT. Only error codes that are currently active will be added to the list. If no error codes are active, an empty list is returned. :return: List of human readable strings. """ ret_val = int(self.query("ERROR"), 2) errors = [] for errcode, errstr in self._error_codes.items(): if ret_val & errcode: errors.append(errstr) return errors @property def ki(self): """Set/get the integral constant of the temperature control loop. The default integral constant is 1.0 and will be reset to this value if the reset method is called. :return: The current integral constant. :rtype: float """ ret_val = self.query("SET KI") return float(ret_val) @ki.setter def ki(self, value): _ = self.query("SET KI", float(value)) @property def kp(self): """Set/get the proportional constant of the temperature control loop. The default proportional constant is 50.0 and will be reset to this value if the reset method is called. :return: The current proportional constant. :rtype: float """ ret_val = self.query("SET KP") return float(ret_val) @kp.setter def kp(self, value): _ = self.query("SET KP", float(value)) @property def power(self): """ Get the current power in Watts. :return: The current power in Watts. """ ret_val = self.query("P") return float(ret_val) * u.W @property def power_current_and_limits(self): """ Get the current power and power limits in Watts. :return: Three u.Quantity objects representing the maximum allowable power at the current temperature, the minimum allowable power at the current temperature, and the current power. """ ret_vals = self.query_multiline("E", 3) max_power = float(ret_vals[0]) * u.W min_power = float(ret_vals[1]) * u.W current_power = float(ret_vals[2]) * u.W return max_power, min_power, current_power @property def power_max(self): """ Get/set the maximum user defined power in Watts. The cooler will automatically limit the power to a safe value if this number exceeds the maximum allowable power. :return: The maximum user defined power in Watts. """ ret_val = self.query("SET MAX") return float(ret_val) * u.W @power_max.setter def power_max(self, value): value = assume_units(value, u.W).to(u.W) if value.magnitude < 0 or value.magnitude > 999.99: raise ValueError("Maximum power must be between 0 and 999.99 Watts.") self.query("SET MAX", float(value.magnitude)) @property def power_min(self): """ Get/set the minimum user defined power in Watts. The cooler will automatically limit the power to a safe value if this number exceeds the minimum allowable power. :return: The minimum user defined power in Watts. """ ret_val = self.query("SET MIN") return float(ret_val) * u.W @power_min.setter def power_min(self, value): value = assume_units(value, u.W).to(u.W) if value.magnitude < 0 or value.magnitude > 999.99: raise ValueError("Minimum power must be between 0 and 999.99 Watts.") self.query("SET MIN", float(value.magnitude)) @property def power_setpoint(self): """ Get/set the setpoint power in Watts. This setpoint is used when the control mode is set to `ControlMode.POWER`. Setting the power is unitful. If no unit is given, it is assumed to be in Watts. While any number from 0 to 999.99 can be set, the controller will only command a power that will not damage the cryocooler. :return: The setpoint power in Watts. :raises ValueError: If the power is set to a value outside the allowed range. """ ret_val = self.query("SET PWOUT") return float(ret_val) * u.W @power_setpoint.setter def power_setpoint(self, value): value = assume_units(value, u.W).to(u.W) if value.magnitude < 0 or value.magnitude > 999.99: raise ValueError("Power setpoint must be between 0 and 999.99 Watts.") self.query("SET PWOUT", float(value.magnitude)) @property def serial_number(self): """ Get the serial number and revision of the CryoTel GT. :return: List of serial number string and revision string. """ return self.query_multiline("SERIAL", 2) @property def state(self): """ Get a list of most of the control parameters and their values. Note: This is the direct list from the CryoTel GT controller. See the manual for the meaning of the parameters. :return: A list of strings representing the control parameters and their values. """ return self.query_multiline("STATE", 14) @property def temperature(self): """ Get the current temperature in Kelvin. :return: The current temperature in Kelvin. """ ret_val = self.query("TC") return float(ret_val) * u.K @property def temperature_setpoint(self): """ Get/set the setpoint temperature in Kelvin. This setpoint is used when the control mode is set to `ControlMode.TEMPERATURE`. Setting the temperature is unitful. If no unit is given, it is assumed to be in Kelvin. :return: The setpoint temperature in Kelvin. """ ret_val = self.query("SET TTARGET") return float(ret_val) * u.K @temperature_setpoint.setter def temperature_setpoint(self, value): value = assume_units(value, u.K).to(u.K) self.query("SET TTARGET", float(value.magnitude)) @property def thermostat(self): """Get/set the thermostat mode of the CryoTel GT. Set this to `True` to enable the thermostat mode, or `False` to disable it. :return: The current thermostat mode state. :rtype: bool """ ret_val = int(float(self.query("SET TSTATM"))) return bool(ret_val) @thermostat.setter def thermostat(self, value): if not isinstance(value, bool): raise ValueError("Invalid thermostat mode. Use True or False.") self.query("SET TSTATM", int(value)) @property def thermostat_status(self): """ Get the current thermostat status of the CryoTel GT. Returns `ThermostatStatus.ON` if the thermostat is enabled, and `ThermostatStatus.OFF` if it is disabled. :return: The current thermostat status. :rtype: ThermostatStatus """ ret_val = int(float(self.query("TSTAT"))) return self.ThermostatStatus(ret_val) @property def stop(self): """ Get/set the stop state of the CryoTel GT. Valid options are `True` (stop) and `False` (start). :return: The current stop state. """ ret_val = int(float(self.query("SET SSTOP"))) return bool(ret_val) @stop.setter def stop(self, value): if not isinstance(value, bool): raise ValueError("Invalid stop state. Use True or False.") self.query("SET SSTOP", int(value)) @property def stop_mode(self): """ Get/set the stop mode of the CryoTel GT. Valid options are `StopMode.HOST` and `StopMode.DIGIO`. :return: The current stop mode. """ ret_val = int(float(self.query("SET SSTOPM"))) return self.StopMode(ret_val) @stop_mode.setter def stop_mode(self, value): if not isinstance(value, self.StopMode): raise ValueError("Invalid stop mode. Use StopMode.HOST or StopMode.DIGIO.") self.query("SET SSTOPM", value.value) # CryoCooler Methods
[docs] def reset(self): """ Reset the CryoTel GT to factory defaults. """ _ = self.query_multiline("RESET=F", 2)
[docs] def save_control_mode(self): """ Save the current control mode as the default control mode. """ _ = self.query("SAVE PID")
# Driver methods
[docs] def query(self, command, value=None): """ Send a query to the cooler and return the response if no value is given. When setting a variable, the CryoTel GT will generally return the value that was set. This is checked for accuracy and a warning is raised if the return value is not the same as the set value. For an actual query where we expect a result, the result is returned unchanged. :param command: The command to send to the cooler. :param value: The value to be set. If not given or None, it is assumed that you want to query the cryocooler. :return: The response from the cooler or None. """ if value is None: self.sendcmd(command) return self.read().strip() else: if isinstance(value, float): value_to_send = f"{value:.2f}" else: value_to_send = str(value) self.sendcmd(f"{command}={value_to_send}") ret_val = self.read().strip() if float(ret_val) != value: warnings.warn( f"Set value {value} does not match returned value {ret_val}." )
[docs] def query_multiline(self, command, num_lines): """ Send a query to the cooler and return the response. This is used for commands that return multiple lines of data. :param command: The command to send to the cooler. :param num_lines: The number of lines to read from the cooler. :return: The response from the cooler as a list of lines. """ self.sendcmd(command) ret_val = [self.read().strip() for _ in range(num_lines)] return ret_val
[docs] def sendcmd(self, command): """ Send a command to the cooler. :param command: The command to send to the cooler. """ self._file.sendcmd(command) _ = self.read() # echo