diff --git a/docs/examples/Lakeshore_625.ipynb b/docs/examples/Lakeshore_625.ipynb new file mode 100644 index 000000000..4924e1eaa --- /dev/null +++ b/docs/examples/Lakeshore_625.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Example with Lakeshore Model 625 Superconducting Magnet power supply" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import qcodes as qc" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from qcodes_contrib_drivers.drivers.Lakeshore.Model_625 import Lakeshore625" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to: LSCI 625 (serial:6251287, firmware:1.3/1.1) in 0.77s\n" + ] + } + ], + "source": [ + "# Initialize a single power supply for a superconducting magnet using GPIB\n", + "# specify coil constant of the magnet in units of T/A\n", + "# specifz field ramp rate in units of T/min\n", + "magnet = Lakeshore625(name = 'magnet', address = 'GPIB0::4::INSTR', coil_constant = 0.0166614, field_ramp_rate = 0.15)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'vendor': 'LSCI', 'model': '625', 'serial': '6251287', 'firmware': '1.3/1.1'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "magnet.IDN()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "magnet:\n", + "\tparameter value\n", + "--------------------------------------------------------------------------------\n", + "IDN :\t{'vendor': 'LSCI', 'model': '625', 'serial': '625...\n", + "coil_constant :\t0.01666 (magnet_coil_constant_unit)\n", + "coil_constant_unit :\tT/A \n", + "current :\t0.0014 (A)\n", + "current_limit :\t55 (A)\n", + "current_ramp_rate :\t0.15 (A/s)\n", + "current_rate_limit :\t0.3 (A/s)\n", + "field :\t2.4e-05 (T)\n", + "field_ramp_rate :\t0.14994 (T/min)\n", + "oer_quench :\tno quench detected \n", + "operational_error_status :\t000000000 \n", + "persistent_switch_heater :\tdisabled \n", + "quench_current_step_limit :\t0.4 (A/s)\n", + "quench_detection :\tenabled \n", + "ramp_segments :\tdisabled \n", + "ramping_state :\tnot ramping \n", + "timeout :\t10 (s)\n", + "voltage :\t0.0048 (V)\n", + "voltage_limit :\t1 (V)\n" + ] + } + ], + "source": [ + "# Let's look at all parameters\n", + "magnet.print_readable_snapshot(update=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Check current to field conversion" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "coil constant = 0.01666 T/A\n", + "current limit = 55.0 A\n", + "current ramp rate limit = 0.3 A/s\n" + ] + } + ], + "source": [ + "# Since the set method of the driver only excepts fields in Tesla and we want to check if the correct \n", + "# currents are applied, we need to convert target currents to target fields. For this reason we need \n", + "# the coil constant. \n", + "coil_const = magnet.coil_constant()\n", + "current_limit = magnet.current_limit()\n", + "current_rate_limit = magnet.current_rate_limit()\n", + "print(\"coil constant = {} T/A\".format(coil_const))\n", + "print(\"current limit = {} A\".format(current_limit))\n", + "print(\"current ramp rate limit = {} A/s\".format(current_rate_limit))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target field is 0.0016660000000000002 T\n", + "Measured field is 0.001678 T\n", + "Measured current is = 0.1007202881152461 A\n" + ] + } + ], + "source": [ + "# Let see if we can set and get the field in Tesla \n", + "target_current = 0.1 # [A] The current we want to set \n", + "target_field = coil_const * target_current # [T]\n", + "print(\"Target field is {} T\".format(target_field))\n", + "magnet.field(target_field)\n", + "\n", + "field = magnet.field() # This gives us the measured field\n", + "print(\"Measured field is {} T\".format(field))\n", + "# The current should be \n", + "current = field / coil_const\n", + "print(\"Measured current is = {} A\".format(current))\n", + "# We have verified with manual inspection that the current has indeed ben reached" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Let's have a look at other parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Field" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.001678" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Let's read the field\n", + "magnet.field()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Let's set a field (blocking mode)\n", + "magnet.field(0.005)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Let's set a field (non-blocking mode)\n", + "magnet.set_field(0.01, block=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ramp rate" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Field ramp rate: 0.09996000000000001 T/min.\n", + "Current ramp rate: 0.1 A/s.\n", + "Field ramp rate: 0.19992000000000001 T/min.\n", + "Current ramp rate: 0.2 A/s.\n" + ] + } + ], + "source": [ + "# The field ramp rate can easily be changed (this will update the current ramp rate as well since the power supply only works in current)\n", + "magnet.field_ramp_rate(0.1)\n", + "print('Field ramp rate: {} {}.'.format(magnet.field_ramp_rate(), magnet.field_ramp_rate.unit))\n", + "print('Current ramp rate: {} {}.'.format(magnet.current_ramp_rate(), magnet.current_ramp_rate.unit))\n", + "\n", + "magnet.field_ramp_rate(0.2)\n", + "print('Field ramp rate: {} {}.'.format(magnet.field_ramp_rate(), magnet.field_ramp_rate.unit))\n", + "print('Current ramp rate: {} {}.'.format(magnet.current_ramp_rate(), magnet.current_ramp_rate.unit))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Quench detection" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Quench detection is enabled.\n", + "Current rate limit for quench detection:0.4 A/s\n", + "There was no no quench detected.\n" + ] + } + ], + "source": [ + "# Quenches detection can be enabled, which it is by default\n", + "print('Quench detection is {}.'.format(magnet.quench_detection()))\n", + "\n", + "# A quench is detected if the current rate is above the follwoing limit\n", + "print('Current rate limit for quench detection:{} {}'.format(magnet.quench_current_step_limit(), magnet.quench_current_step_limit.unit))\n", + "\n", + "# Let's check if there was a quench detected\n", + "print('There was no {}.'.format(magnet.oer_quench()))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + }, + "nbsphinx": { + "execute": "never" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/qcodes_contrib_drivers/drivers/Lakeshore/Model_625.py b/qcodes_contrib_drivers/drivers/Lakeshore/Model_625.py new file mode 100644 index 000000000..bcd0436d3 --- /dev/null +++ b/qcodes_contrib_drivers/drivers/Lakeshore/Model_625.py @@ -0,0 +1,523 @@ +import logging +import time +from typing import Union, Tuple + + +from qcodes import VisaInstrument +from qcodes.utils.validators import Numbers, Enum + + +class Lakeshore625(VisaInstrument): + """ + Driver for the Lakeshore Model 625 superconducting magnet power supply. + + This class uses T/A and A/s as units. + + Args: + name (str): a name for the instrument + coil_constant (float): Coil contant of magnet, in untis of T/A + field_ramp_rate (float): Magnetic field ramp rate, in units of T/min + address (str): VISA address of the device + """ + + def __init__(self, name: str, coil_constant: float, field_ramp_rate: float, address: str, + reset: bool=False, terminator:str='', **kwargs) -> None: + + super().__init__(name, address, terminator=terminator, **kwargs) + + # Add reset function + self.add_function('reset', call_cmd='*RST') + if reset: + self.reset() + + # Add power supply parameters + self.add_parameter(name='current_limit', + unit="A", + set_cmd=self._set_curent_limit, + get_cmd=self._get_current_limit, + get_parser=float, + vals=Numbers(0, 60.1), + docstring="Maximum output current" + ) + + self.add_parameter(name='voltage_limit', + unit="V", + set_cmd=self._set_voltage_limit, + get_cmd=self._get_voltage_limit, + get_parser=float, + vals=Numbers(0, 5), + docstring="Maximum compliance voltage" + ) + + self.add_parameter(name='current_rate_limit', + unit="A/s", + set_cmd=self._set_current_rate_limit, + get_cmd=self._get_current_rate_limit, + get_parser=float, + vals=Numbers( 0.0001, 99.999), + docstring="Maximum current ramp rate" + ) + + self.add_parameter(name='voltage', + unit = 'V', + set_cmd="SETV {}", + get_cmd='RDGV?', + get_parser=float, + vals=Numbers(-5, 5) + ) + + self.add_parameter(name='current', + unit = 'A', + set_cmd="SETI {}", + get_cmd='RDGI?', + get_parser=float, + vals=Numbers(-60, 60) + ) + + self.add_parameter(name='current_ramp_rate', + unit = 'A/s', + set_cmd="RATE {}", + get_cmd='RATE?', + get_parser=float + ) + + self.add_parameter(name='ramp_segments', + set_cmd="RSEG {}", + get_cmd='RSEG?', + get_parser=int, + val_mapping={'disabled': 0, + 'enabled': 1} + ) + + self.add_parameter(name='persistent_switch_heater', + set_cmd=self._set_persistent_switch_heater_status, + get_cmd=self._get_persistent_switch_heater_status, + get_parser=int, + val_mapping={'disabled': 0, + 'enabled': 1} + ) + + self.add_parameter(name='quench_detection', + set_cmd=self._set_quench_detection_status, + get_cmd=self._get_quench_detection_status, + get_parser=int, + val_mapping={'disabled': 0, + 'enabled': 1} + ) + + self.add_parameter(name='quench_current_step_limit', + unit = 'A/s', + set_cmd=self._set_quench_current_step_limit, + get_cmd=self._get_quench_current_step_limit, + get_parser=float, + vals=Numbers(0.01, 10) + ) + + self.add_parameter(name='ramping_state', + get_cmd=self._get_ramping_state, + vals=Enum('ramping', 'not ramping') + ) + + self.add_parameter(name='operational_error_status', + get_cmd=self._get_operational_errors, + get_parser=str + ) + + self.add_parameter(name='oer_quench', + get_cmd=self._get_oer_quench_bit, + get_parser=int, + val_mapping={'no quench detected': 0, + 'quench detected': 1} + ) + + + # Add solenoid parameters + self.add_parameter(name='coil_constant_unit', + set_cmd=self._set_coil_constant_unit, + get_cmd=self._get_coil_constant_unit, + get_parser=int, + val_mapping={'T/A': 0, + 'kG/A': 1}, + docstring="unit of the coil constant, either T/A (default) or kG/A" + ) + + self.add_parameter(name='coil_constant', + unit = self.coil_constant_unit, + set_cmd=self._update_coil_constant, + get_cmd=self._get_coil_constant, + get_parser=float, + vals=Numbers(0.001, 999.99999) # what are good numbers here? + ) + + self.add_parameter(name='field', + unit = 'T', + set_cmd=self.set_field, + get_cmd='RDGF?', + get_parser=float + ) + + self.add_parameter(name='field_ramp_rate', + unit = 'T/min', + set_cmd=self._set_field_ramp_rate, + get_cmd=self._get_field_ramp_rate, + get_parser=float, + docstring="Field ramp rate (T/min)" + ) + + + # Add clear function + self.add_function('clear', call_cmd='*CLS') + + # disable persistent switch heater by default + self.persistent_switch_heater('disabled') + + # disable ramp segments by default + self.ramp_segments('disabled') + + # set coil constant unit to T/A by default + self.coil_constant_unit('T/A') + + # assign init parameters + self.coil_constant(coil_constant) + self.field_ramp_rate(field_ramp_rate) + + # print connect message + self.connect_message() + + + def _sleep(self, t: float) -> None: + """ + Sleep for a number of seconds t. If we are or using + the PyVISA 'sim' backend, omit this + """ + simmode = getattr(self, 'visabackend', False) == 'sim' + + if simmode: + return + else: + time.sleep(t) + + + # get functions returning several values + def _get_limit(self) -> Tuple[float, float, float]: + """ + Limit Output Settings Query + + Returns + ------- + , , + """ + raw_string = self.ask('LIMIT?') + current_limit, voltage_limit, current_rate_limit = raw_string.split(',') + return float(current_limit), float(voltage_limit), float(current_rate_limit) + + + def _get_persistent_switch_heater_setup(self) -> Tuple[int, float, float]: + """ + Persistent Switch Heater Parameter Query + + Returns + ------- + , , + """ + raw_string = self.ask('PSHS?') + status, psh_current, psh_delay = raw_string.split(',') + return int(status), float(psh_current), float(psh_delay) + + + def _get_quench_detection_setup(self) -> Tuple[int, float]: + """ + Quench Parameter Query + + Returns + ------- + , + """ + raw_string = self.ask('QNCH?') + status, current_step_limit = raw_string.split(',') + return int(status), float(current_step_limit) + + + def _get_field_setup(self) -> Tuple[str, float]: + """ + Computed Magnetic Field Parameter Query + + Returns + ------- + , + """ + raw_string = self.ask('FLDS?') + unit, coil_constant = raw_string.split(',') + return str(unit), float(coil_constant) + + + # get functions for parameters + def _get_current_limit(self) -> float: + """ + Get maximum allowed output current setting. + + Returns + ------- + + """ + current_limit, voltage_limit, current_rate_limit = self._get_limit() + return current_limit + + + def _get_voltage_limit(self) -> float: + """ + Gets maximum allowed compliance voltage setting + + Returns + ------- + + """ + current_limit, voltage_limit, current_rate_limit = self._get_limit() + return voltage_limit + + + def _get_current_rate_limit(self) -> float: + """ + Gets maximum allowed output current ramp rate setting + + Returns + ------- + + """ + current_limit, voltage_limit, current_rate_limit = self._get_limit() + return current_rate_limit + + + def _get_persistent_switch_heater_status(self) -> int: + """ + Queries if there is a persistent switch: 0 = Disabled (no PSH), 1 = Enabled + + Returns + ------- + status + """ + status, psh_current, psh_delay = self._get_persistent_switch_heater_setup() + return status + + + def _get_quench_detection_status(self) -> int: + """ + Queries if quench detection is to be used: 0 = Disabled, 1 = Enabled + + Returns + ------- + status + """ + status, current_step_limit = self._get_quench_detection_setup() + return status + + + def _get_quench_current_step_limit(self) -> float: + """ + Gets current step limit for quench detection + + Returns + ------- + + """ + status, current_step_limit = self._get_quench_detection_setup() + return current_step_limit + + + def _get_coil_constant(self) -> float: + """ + Gets magnetic field constant in either T/A or kG/A depending on units + + Returns + ------- + + """ + coil_constant_unit, coil_constant = self._get_field_setup() + return coil_constant + + + def _get_coil_constant_unit(self) -> str: + """ + Gets the units of the magnetic field constant: 0 = T/A, 1 = kG/A + + Returns + ------- + + """ + coil_constant_unit, coil_constant = self._get_field_setup() + return coil_constant_unit + + + def _get_field_ramp_rate(self) -> float: + """ + Gets the field ramp rate in units of T/min + + Returns + ------- + field_ramp_rate (T/min) + """ + coil_constant_unit, coil_constant = self._get_field_setup() # in T/A by default + current_ramp_rate = self.current_ramp_rate() # in A/s + field_ramp_rate = current_ramp_rate * coil_constant * 60 # in T/min + return field_ramp_rate + + + def _get_ramping_state(self) -> str: + """ + Gets the ramping state of the power supply (corresponds to blue LED on panel) + Is inferred from the status bit register + + Returns + ------- + ramping state + """ + operation_condition_register = self.ask('OPST?') + bin_OPST = bin(int(operation_condition_register))[2:] + if len(bin_OPST)<2: + rampbit = 1 + else: + # read second bit, 0 = ramping, 1 = not ramping + rampbit = int(bin_OPST[-2]) + if rampbit == 1: + return 'not ramping' + else: + return 'ramping' + + + def _get_operational_errors(self) -> str: + """ + Error Status Query + + Returns + ------- + error status + """ + error_status_register = self.ask('ERST?') + # three bytes are read at the same time, the middle one is the operational error status + operational_error_registor = error_status_register.split(',')[1] + + #prepend zeros to bit-string such that it always has length 9 + oer_bit_str = bin(int(operational_error_registor))[2:].zfill(9) + return oer_bit_str + + + def _get_oer_quench_bit(self) -> int: + """ + Returns the oer quench bit + + Returns + ------- + quench bit + """ + return int(self._get_operational_errors()[3]) + + + # set functions for parameters + def _set_curent_limit(self, current_limit_setpoint: float) -> None: + """ + Sets maximum allowed output current + """ + current_limit, voltage_limit, current_rate_limit = self._get_limit() + self.write_raw('LIMIT {}, {}, {}'.format(current_limit_setpoint, voltage_limit, current_rate_limit)) + + + def _set_voltage_limit(self, voltage_limit_setpoint: float) -> None: + """ + Sets maximum allowed compliance voltage + """ + current_limit, voltage_limit, current_rate_limit = self._get_limit() + self.write_raw('LIMIT {}, {}, {}'.format(current_limit, voltage_limit_setpoint, current_rate_limit)) + + + def _set_current_rate_limit(self, current_rate_limit_setpoint: float) -> None: + """ + Sets maximum allowed output current ramp rate + """ + current_limit, voltage_limit, current_rate_limit = self._get_limit() + self.write_raw('LIMIT {}, {}, {}'.format(current_limit, voltage_limit, current_rate_limit_setpoint)) + + + def _set_persistent_switch_heater_status(self, status_setpoint: int) -> None: + """ + Specifies if there is a persistent switch: 0 = Disabled (no PSH), 1 = Enabled + """ + status, psh_current, psh_delay = self._get_persistent_switch_heater_setup() + self.write_raw('PSHS {}, {}, {}'.format(status_setpoint, psh_current, psh_delay)) + + + def _set_quench_detection_status(self, status_setpoint: int) -> None: + """ + Specifies if quench detection is to be used: 0 = Disabled, 1 = Enabled + """ + status, current_step_limit = self._get_quench_detection_setup() + self.write_raw('QNCH {}, {}'.format(status_setpoint, current_step_limit)) + + + def _set_quench_current_step_limit(self, current_step_limit_setpoint: float) -> None: + """ + Specifies the current step limit for quench detection + """ + status, current_step_limit = self._get_quench_detection_setup() + self.write_raw('QNCH {}, {}'.format(status, current_step_limit_setpoint)) + + + def _set_coil_constant(self, coil_constant_setpoint: float) -> None: + """ + Specifies the magnetic field constant in either T/A or kG/A depending on units + """ + coil_constant_unit, coil_constant = self._get_field_setup() + self.write_raw('FLDS {}, {}'.format(coil_constant_unit, coil_constant_setpoint)) + + + def _set_coil_constant_unit(self, coil_constant_unit_setpoint: str) -> None: + """ + Specifies the units of the magnetic field constant: 0 = T/A, 1 = kG/A + """ + coil_constant_unit, coil_constant = self._get_field_setup() + self.write_raw('FLDS {}, {}'.format(coil_constant_unit_setpoint, coil_constant)) + + + def _update_coil_constant(self, coil_constant_setpoint: float) -> None: + """ + Updates the coil_constant and with it all linked parameters + """ + # read field_ramp_rate before chaning coil constant + field_ramp_rate = self.field_ramp_rate() + # set the coil constant + self._set_coil_constant(coil_constant_setpoint) + # update the current ramp rate, leaving the field ramp rate unchanged + current_ramp_rate_setpoint = field_ramp_rate / coil_constant_setpoint / 60 # current_ramp_rate is in A/s + self.current_ramp_rate(current_ramp_rate_setpoint) + + + def _set_field_ramp_rate(self, field_ramp_rate_setpoint: float) -> None: + """ + Sets the field ramp rate in units of T/min by setting the corresponding current_ramp_rate + """ + coil_constant_unit, coil_constant = self._get_field_setup() # in T/A by default + current_ramp_rate_setpoint = field_ramp_rate_setpoint / coil_constant / 60 # current_ramp_rate is in A/s + self.current_ramp_rate(current_ramp_rate_setpoint) + + + def set_field(self, value: float, block: bool=True) -> None: + """ + Ramp to a certain field + + Args: + value: field setpoint + block: Whether to wait until the field has finished setting + """ + + self.write('SETF {}'.format(value)) + # Check if we want to block + if not block: + return + + # Otherwise, wait until no longer ramping + self.log.debug(f'Starting blocking ramp of {self.name} to {value}') + self._sleep(0.5) # wait for a short time for the power supply to fall into the ramping state + while self.ramping_state() == 'ramping': + self._sleep(0.3) + self._sleep(2.0) + self.log.debug(f'Finished blocking ramp') + return