Connecting to Lakeshore Model 325 by Lakeshore in Python
Instrument Card
The Model 325 dual-channel cryogenic temperature controller is capable of supporting nearly any diode, RTD, or thermocouple temperature sensor. Two independent PID control loops with heater outputs of 25 W and 2 W are configured to drive either a 50 Ω or 25 Ω load for optimal cryocooler control flexibility. Designed with ease of use, functionality, and value in mind, the Model 325 is ideal for general-purpose laboratory and industrial temperature measurement and control applications.

Device Specification: here
Manufacturer card: LAKESHORE

Supporting advanced scientific research, Lake Shore is a leading global innovator in measurement and control solutions.
- Headquarters: Westerville, Ohio, USA
- Yearly Revenue (millions, USD): 21.4
- Vendor Website: here
Connect to the Lakeshore Model 325 in Python
Read our guide for turning Python scripts into Flojoy nodes.
PROTOCOLS > SCPI
Here is a Python script that uses Qcodes to connect to a Lakeshore Model 325 Temperature Controller:
from qcodes.instrument.visa import VisaInstrumentfrom qcodes.instrument.channel import ChannelListfrom qcodes.instrument.parameter import Parameterfrom qcodes.utils.validators import Enum, Numbersfrom qcodes import InstrumentChannel, VisaInstrument
class LakeshoreModel325Status(IntFlag):    """    IntFlag that defines status codes for Lakeshore Model 325    """    sensor_units_overrang = 128    sensor_units_zero = 64    temp_overrange = 32    temp_underrange = 16    invalid_reading = 1
class LakeshoreModel325Curve(InstrumentChannel):    """    An InstrumentChannel representing a curve on a Lakeshore Model 325    """
    valid_sensor_units = ["mV", "V", "Ohm", "log Ohm"]    temperature_key = "Temperature (K)"
    def __init__(self, parent: "LakeshoreModel325", index: int) -> None:
        self._index = index        name = f"curve_{index}"        super().__init__(parent, name)
        self.add_parameter("serial_number", parameter_class=Parameter)
        self.add_parameter(            "format",            val_mapping={                f"{unt}/K": i + 1 for i, unt in enumerate(self.valid_sensor_units)            },            parameter_class=Parameter,        )
        self.add_parameter("limit_value", parameter_class=Parameter)
        self.add_parameter(            "coefficient",            val_mapping={"negative": 1, "positive": 2},            parameter_class=Parameter,        )
        self.add_parameter("curve_name", parameter_class=Parameter)
        Group(            [                self.curve_name,                self.serial_number,                self.format,                self.limit_value,                self.coefficient,            ],            set_cmd=f"CRVHDR {self._index}, {{curve_name}}, "            f"{{serial_number}}, {{format}}, {{limit_value}}, "            f"{{coefficient}}",            get_cmd=f"CRVHDR? {self._index}",        )
    def get_data(self) -> dict[Any, Any]:        curve = [            float(a)            for point_index in range(1, 200)            for a in self.ask(f"CRVPT? {self._index}, {point_index}").split(",")        ]
        d = {self.temperature_key: curve[1::2]}        sensor_unit = self.format().split("/")[0]        d[sensor_unit] = curve[::2]
        return d
    @classmethod    def validate_datadict(cls, data_dict: dict[Any, Any]) -> str:        """        A data dict has two keys, one of which is 'Temperature (K)'. The other        contains the units in which the curve is defined and must be one of:        'mV', 'V', 'Ohm' or 'log Ohm'
        This method validates this and returns the sensor unit encountered in        the data dict        """        if cls.temperature_key not in data_dict:            raise ValueError(                f"At least {cls.temperature_key} needed in the " f"data dictionary"            )
        sensor_units = [i for i in data_dict.keys() if i != cls.temperature_key]
        if len(sensor_units) != 1:            raise ValueError(                "Data dictionary should have one other key, other then "                "'Temperature (K)'"            )
        sensor_unit = sensor_units[0]
        if sensor_unit not in cls.valid_sensor_units:            raise ValueError(                f"Sensor unit {sensor_unit} invalid. This needs to be one of "                f"{', '.join(cls.valid_sensor_units)}"            )
        data_size = len(data_dict[cls.temperature_key])        if data_size != len(data_dict[sensor_unit]) or data_size > 200:            raise ValueError(                "The length of the temperature axis should be "                "the same as the length of the sensor axis and "                "should not exceed 200 in size"            )
        return sensor_unit
    def set_data(        self, data_dict: dict[Any, Any], sensor_unit: Optional[str] = None    ) -> None:        """        Set the curve data according to the values found the the dictionary.
        Args:            data_dict (dict): See `validate_datadict` to see the format of this                                dictionary            sensor_unit (str): If None, the data dict is validated and the                                units are extracted.        """        if sensor_unit is None:            sensor_unit = self.validate_datadict(data_dict)
        temperature_values = data_dict[self.temperature_key]        sensor_values = data_dict[sensor_unit]
        for value_index, (temperature_value, sensor_value) in enumerate(            zip(temperature_values, sensor_values)        ):
            cmd_str = (                f"CRVPT {self._index}, {value_index + 1}, "                f"{sensor_value:3.3f}, {temperature_value:3.3f}"            )
            self.write(cmd_str)
class LakeshoreModel325Sensor(InstrumentChannel):    """    InstrumentChannel for a single sensor of a Lakeshore Model 325.
    Args:        parent (LakeshoreModel325): The instrument this heater belongs to        name (str)        inp (str): Either "A" or "B"    """
    def __init__(self, parent: "LakeshoreModel325", name: str, inp: str) -> None:
        if inp not in ["A", "B"]:            raise ValueError("Please either specify input 'A' or 'B'")
        super().__init__(parent, name)        self._input = inp
        self.add_parameter(            "temperature",            get_cmd=f"KRDG? {self._input}",            get_parser=float,            label="Temperature",            unit="K",        )
        self.add_parameter(            "status",            get_cmd=f"RDGST? {self._input}",            get_parser=lambda status: self.decode_sensor_status(int(status)),            label="Sensor_Status",        )
        self.add_parameter(            "type",            val_mapping={                "Silicon diode": 0,                "GaAlAs diode": 1,                "100 Ohm platinum/250": 2,                "100 Ohm platinum/500": 3,                "1000 Ohm platinum": 4,                "NTC RTD": 5,                "Thermocouple 25mV": 6,                "Thermocouple 50 mV": 7,                "2.5 V, 1 mA": 8,                "7.5 V, 1 mA": 9,            },            parameter_class=Parameter,        )
        self.add_parameter(            "compensation", vals=Enum(0, 1), parameter_class=Parameter        )
        Group(            [self.type, self.compensation],            set_cmd=f"INTYPE {self._input}, {{type}}, {{compensation}}",            get_cmd=f"INTYPE? {self._input}",        )
        self.add_parameter(            "curve_index",            set_cmd=f"INCRV {self._input}, {{}}",            get_cmd=f"INCRV? {self._input}",            get_parser=int,            vals=Numbers(min_value=1, max_value=35),        )
    @staticmethod    def decode_sensor_status(sum_of_codes: int) -> str:        total_status = LakeshoreModel325Status(sum_of_codes)        if sum_of_codes == 0:            return "OK"        status_messages = [            st.name.replace("_", " ")            for st in LakeshoreModel325Status            if st in total_status and st.name is not None        ]        return ", ".join(status_messages)
    @property    def curve(self) -> LakeshoreModel325Curve:        parent = cast(LakeshoreModel325, self.parent)        return LakeshoreModel325Curve(parent, self.curve_index())
class LakeshoreModel325Heater(InstrumentChannel):    """    InstrumentChannel for heater control on a Lakeshore Model 325.
    Args:        parent (LakeshoreModel325): The instrument this heater belongs to        name (str)        loop (int): Either 1 or 2    """
    def __init__(self, parent: "LakeshoreModel325", name: str, loop: int) -> None:
        if loop not in [1, 2]:            raise ValueError("Please either specify loop 1 or 2")
        super().__init__(parent, name)        self._loop = loop
        self.add_parameter(            "control_mode",            get_cmd=f"CMODE? {self._loop}",            set_cmd=f"CMODE {self._loop},{{}}",            val_mapping={                "Manual PID": "1",                "Zone": "2",                "Open Loop": "3",                "AutoTune PID": "4",                "AutoTune PI": "5",                "AutoTune P": "6",            },        )
        self.add_parameter(            "input_channel", vals=Enum("A", "B"), parameter_class=Parameter        )
        self.add_parameter(            "unit",            val_mapping={"Kelvin": "1", "Celsius": "2", "Sensor Units": "3"},            parameter_class=Parameter,        )
        self.add_parameter(            "powerup_enable",            val_mapping={True: 1, False: 0},            parameter_class=Parameter,        )
        self.add_parameter(            "output_metric",            val_mapping={                "current": "1",                "power": "2",            },            parameter_class=Parameter,        )
        Group(            [self.input_channel, self.unit, self.powerup_enable, self.output_metric],            set_cmd=f"CSET {self._loop}, {{input_channel}}, {{unit}}, "            f"{{powerup_enable}}, {{output_metric}}",            get_cmd=f"CSET? {self._loop}",        )
        self.add_parameter(            "P", vals=Numbers(0, 1000), get_parser=float, parameter_class=Parameter        )
        self.add_parameter(            "I", vals=Numbers(0, 1000), get_parser=float, parameter_class=Parameter        )
        self.add_parameter(            "D", vals=Numbers(0, 1000), get_parser=float, parameter_class=Parameter        )
        Group(            [self.P, self.I, self.D],            set_cmd=f"PID {self._loop}, {{P}}, {{I}}, {{D}}",            get_cmd=f"PID? {self._loop}",        )
        if self._loop == 1:            valid_output_ranges = Enum(0, 1, 2)        else:            valid_output_ranges = Enum(0, 1)
        self.add_parameter(            "output_range",            vals=valid_output_ranges,            set_cmd=f"RANGE {self._loop}, {{}}",            get_cmd=f"RANGE? {self._loop}",            val_mapping={"Off": "0", "Low (2.5W)": "1", "High (25W)": "2"},        )
        self.add_parameter(            "setpoint",            vals=Numbers(0, 400),            get_parser=float,            set_cmd=f"SETP {self._loop}, {{}}",            get_cmd=f"SETP? {self._loop}",        )
        self.add_parameter(            "ramp_state", vals=Enum(0, 1), parameter_class=Parameter        )
        self.add_parameter(            "ramp_rate",            vals=Numbers(0, 100 / 60 * 1e3),            unit="mK/s",            parameter_class=Parameter,            get_parser=lambda v: float(v) / 60 * 1e3,  # We get values in K/min,            set_parser=lambda v: v * 60 * 1e-3,  # Convert to K/min        )
        Group(            [self.ramp_state, self.ramp_rate],            set_cmd=f"RAMP {self._loop}, {{ramp_state}}, {{ramp_rate}}",            get_cmd=f"RAMP? {self._loop}",        )
        self.add_parameter("is_ramping", get_cmd=f"RAMPST? {self._loop}")
        self.add_parameter(            "resistance",            get_cmd=f"HTRRES? {self._loop}",            set_cmd=f"HTRRES {self._loop}, {{}}",            val_mapping={                25: 1,                50: 2,            },            label="Resistance",            unit="Ohm",        )
        self.add_parameter(            "heater_output",            get_cmd=f"HTR? {self._loop}",            get_parser=float,            label="Heater Output",            unit="%",        )
class LakeshoreModel325(VisaInstrument):    """    QCoDeS driver for Lakeshore Model 325 Temperature Controller.    """
    def __init__(self, name: str, address: str, **kwargs: Any) -> None:        super().__init__(name, address, terminator="\r\n", **kwargs)
        sensors = ChannelList(            self, "sensor", LakeshoreModel325Sensor, snapshotable=False        )
        for inp in ["A", "B"]:            sensor = LakeshoreModel325Sensor(self, f"sensor_{inp}", inp)            sensors.append(sensor)            self.add_submodule(f"sensor_{inp}", sensor)
        self.add_submodule("sensor", sensors.to_channel_tuple())
        heaters = ChannelList(            self, "heater", LakeshoreModel325Heater, snapshotable=False        )
        for loop in [1, 2]:            heater = LakeshoreModel325Heater(self, f"heater_{loop}", loop)            heaters.append(heater)            self.add_submodule(f"heater_{loop}", heater)
        self.add_submodule("heater", heaters.to_channel_tuple())
        curves = ChannelList(self, "curve", LakeshoreModel325Curve, snapshotable=False)
        for curve_index in range(1, 35):            curve = LakeshoreModel325Curve(self, curve_index)            curves.append(curve)
        self.add_submodule("curve", curves)
        self.connect_message()
    def upload_curve(        self, index: int, name: str, serial_number: str, data_dict: dict[Any, Any]    ) -> None:        """        Upload a curve to the given index
        Args:             index: The index to upload the curve to. We can only use                            indices reserved for user defined curves, 21-35             name             serial_number             data_dict: A dictionary containing the curve data        """        if index not in range(21, 36):            raise ValueError("index value should be between 21 and 35")
        sensor_unit = LakeshoreModel325Curve.validate_datadict(data_dict)
        curve = self.curve[index - 1]        curve.curve_name(name)        curve.serial_number(serial_number)        curve.format(f"{sensor_unit}/K")        curve.set_data(data_dict, sensor_unit=sensor_unit)
    def upload_curve_from_file(self, index: int, file_path: str) -> None:        """        Upload a curve from a curve file. Note that we only support        curve files with extension .330        """        if not file_path.endswith(".330"):            raise ValueError("Only curve files with extension .330 are supported")
        with open(file_path) as curve_file:            file_data = _read_curve_file(curve_file)
        data_dict = _get_sanitize_data(file_data)        name = file_data["metadata"]["Sensor Model"]        serial_number = file_data["metadata"]["Serial Number"]
        self.upload_curve(index, name, serial_number, data_dict)
# Connect to the Lakeshore Model 325 Temperature Controllerlakeshore = LakeshoreModel325("lakeshore", "TCPIP::192.168.1.1::INSTR")
# Access the sensorssensor_A = lakeshore.sensor_Asensor_B = lakeshore.sensor_B
# Read the temperature from sensor Atemperature_A = sensor_A.temperature()print(f"Temperature from sensor A: {temperature_A} K")
# Read the temperature from sensor Btemperature_B = sensor_B.temperature()print(f"Temperature from sensor B: {temperature_B} K")
# Access the heatersheater_1 = lakeshore.heater_1heater_2 = lakeshore.heater_2
# Set the setpoint for heater 1heater_1.setpoint(300)
# Set the setpoint for heater 2heater_2.setpoint(350)
# Upload a curve to the Lakeshore Model 325curve_data = {    "Temperature (K)": [100, 200, 300, 400],    "mV": [0.1, 0.2, 0.3, 0.4]}lakeshore.upload_curve(21, "Curve 1", "12345", curve_data)
# Upload a curve from a filelakeshore.upload_curve_from_file(22, "curve_file.330")Note: This script assumes that you have the necessary Qcodes library installed and that you have the correct address for the Lakeshore Model 325 Temperature Controller.