"""
Driver for Cryogenic Magnet Power Supply SMS120C.
Please refer to Cryogenic's Magnet Power Supply SMS120C manual for further
details and functionality. This magnet PS model is not SCPI compliant. Note
that some commands return more than one line in the output, some are
unidirectional, with no return (eg. 'write' rather than 'ask').
This magnet PS driver has been tested with FTDI chip drivers (USB to serial),
D2XX version installed and with Cryogenic SMS120C and SMS60C (though the
default init arguments are not correct for the latter). Both the
coil_constant and current_rating should be based on calibration data
accompanying the magnet. The SMS60C current_rating should be slightly below
60, as indicated by its name. Examples of values for a 2T magnet using
SMS60C are: coil_constant=0.0380136, current_rating=52.61
"""
import re
import logging
import time
from qcodes.utils.validators import Numbers
from qcodes import VisaInstrument
import pyvisa.constants as vi_const
log = logging.getLogger(__name__)
[docs]
class CryogenicSMS120C(VisaInstrument):
"""
The following hard-coded, default values for Cryogenic magnets are safety limits
and should not be modified.
- these values should be set using the corresponding arguments when the class is called.
"""
default_current_ramp_limit = 0.0506 # [A/s]
default_max_current_ramp_limit = 0.12 # [A/s]
"""
Driver for the Cryogenic SMS120C magnet power supply.
This class controls a single magnet PSU.
Magnet and magnet PSU limits : max B=12T, I=105.84A, V=3.5V
Args:
name (str): a name for the instrument
address (str): (serial to USB) COM number of the power supply
coil_constant (float): coil constant in Tesla per ampere, fixed at 0.113375T/A
current_rating (float): maximum current rating in ampere, fixed at 105.84A
current_ramp_limit (float): current ramp limit in ampere per second,
for 50mK operation 0.0506A/s (5.737E-3 T/s, 0.34422T/min) - usually used
for 4K operation 0.12A/s (0.013605 T/s, 0.8163 T/min) - not recommended
Note about timing : SMS120C needs a minimum of 200ms delay between commands being sent
"""
# Reg. exp. to match a float or exponent in a string
_re_float_exp = r'[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?'
def __init__(self, name, address, coil_constant=0.113375, current_rating=105.84,
current_ramp_limit=0.0506, reset=False, timeout=5, **kwargs):
log.debug('Initializing instrument')
if 'terminator' in kwargs.keys():
kwargs.pop('terminator')
log.warning('Passing terminator to CryogenicSMS is no longer supported and has no effect')
super().__init__(name, address, terminator='\r\n', **kwargs)
self.visa_handle.baud_rate = 9600
self.visa_handle.parity = vi_const.Parity.none
self.visa_handle.stop_bits = vi_const.StopBits.one
self.visa_handle.data_bits = 8
self.visa_handle.flow_control = 0
self.visa_handle.flush(vi_const.VI_READ_BUF_DISCARD |
vi_const.VI_WRITE_BUF_DISCARD) # keep for debugging
idn = self.IDN.get()
print(idn)
self._persistentField = 0 # temp code stub
self._coil_constant = coil_constant
self._current_rating = current_rating
self._current_ramp_limit = current_ramp_limit
self._field_rating = coil_constant * \
current_rating # corresponding max field based
self._field_ramp_limit = coil_constant * current_ramp_limit
self.add_parameter(name='unit',
get_cmd=self._get_unit,
set_cmd=self._set_unit,
val_mapping={'AMPS': 0, 'TESLA': 1})
self.add_parameter('rampStatus',
get_cmd=self._get_rampStatus,
val_mapping={'HOLDING': 0,
'RAMPING': 1,
'QUENCH DETECTED': 2,
'EXTERNAL TRIP': 3,
'FAULT': 4,
})
self.add_parameter('polarity',
get_cmd=self._get_polarity,
set_cmd=self._set_polarity,
val_mapping={'POSITIVE': '+', 'NEGATIVE': '-'})
self.add_parameter(name='switchHeater',
get_cmd=self._get_switchHeater,
set_cmd=self._set_switchHeater,
val_mapping={False: 0, True: 1})
self.add_parameter('persistentMode',
get_cmd=self._get_persistentMode,
set_cmd=self._set_persistentMode,
val_mapping={False: 0, True: 1})
self.add_parameter(name='persistentField',
get_cmd=self._get_persistentField,
vals=Numbers(self._persistentField))
self.add_parameter(name='field',
get_cmd=self._get_field,
set_cmd=self._set_field,
vals=Numbers(-self._field_rating, # i.e. ~12T, calculated
self._field_rating))
self.add_parameter(name='maxField',
get_cmd=self._get_maxField,
set_cmd=self._set_maxField,
vals=Numbers(0, # i.e. ~12T, calculated
self._field_rating))
self.add_parameter(name='rampRate',
get_cmd=self._get_rampRate,
set_cmd=self._set_rampRate,
vals=Numbers(0,
self._current_ramp_limit))
self.add_parameter('pauseRamp',
set_cmd=self._set_pauseRamp,
get_cmd=self._get_pauseRamp,
val_mapping={False: 0, True: 1})
[docs]
def get_idn(self):
"""
Overwrites the get_idn function using constants as the hardware
does not have a proper ``*IDN`` function.
"""
idparts = ['Cryogenic', 'Magnet PS SMS120C', 'None', '1.0']
return dict(zip(('vendor', 'model', 'serial', 'firmware'), idparts))
[docs]
def query(self, msg):
"""
Message outputs do not follow the standard SCPI format,
separate regexp to parse unique/variable instrument message structures.
Returns:
The key (unused), and the value (parsed value extracted from output
message)
"""
value = self.ask(msg)
#BUG: The SMS60C sometimes returns an incorrect \x13 at the beginning of the string
value = value.strip('\x13')
m = re.match(r'((\S{8})\s)+(([^:]+)(:([^:]+))?)', value)
if m:
if m[2] == '------->':
log.error(
'Command information or unrecognizable qualifier: "%s"' % m[3])
return None, None
else:
return m[4].strip(), m[6].strip()
else:
log.error(
'Malformed message received from the magnet PS: "%s"' % value)
return None, None
def _get_limit(self): # Get voltage limits, returns a float
_, value = self.query('GET VL')
# extract number from string
m = re.match(r'({}) VOLTS'.format(
CryogenicSMS120C._re_float_exp), value)
limit = float(m[1])
return limit
# get heater status, returns a boolean ON (1) or OFF (0)
def _get_switchHeater(self):
_, value = self.query('HEATER')
if 'OFF' in value:
switchHeater = 0
elif 'ON' in value:
switchHeater = 1
return switchHeater
# check if magnet is in persistent mode, and if so return current in the
# magnet
def _get_persistentMode(self):
_, value = self.query('HEATER')
field = self._get_field()
# check for switch heater OFF, and non-zero current
if 'OFF' in value and abs(field <= 0.007):
persistentField = self._get_persistentField()
units = self._get_unit()
if units == 1:
log.info("Magnet in persistent mode, at a field of %f T" %
persistentField)
elif units == 0:
log.info("Magnet in persistent mode, at a field of %f A" %
persistentField)
persistentMode = True
else:
log.info("Magnet not persistent.")
persistentMode = False
return persistentMode
# get units, returns a boolean integer - Tesla (1) or Amps(0)
def _get_persistentField(self):
# read persistent field from controller
BLeads = self._get_field()
if (self._get_switchHeater() == 1):
log.info("Switch heater ON, magnet not in persistent mode.")
persistentField = 0
elif (self._get_switchHeater() == 0) and (abs(BLeads) > 0.007):
log.info(
"Switch heater OFF, but current is still in present in leads - not in persistent mode. Leads at: %f" % BLeads)
perString = self.ask('GET PER')
m = re.match(r'((\S{8})\s)+([^:]+)', perString)
persistentField = float(m[2])
else:
perString = self.ask('GET PER')
# handles a different string return format
m = re.match(r'((\S{8})\s)+([^:]+)', perString)
persistentField = float(m[2])
return persistentField
def _get_unit(self): # get units, returns a boolean integer - Tesla (1) or Amps(0)
_, value = self.query('TESLA')
if value == 'TESLA':
unit = 1
else: # assume in Amps
unit = 0
return unit
# get direction of current, returns a string - Positive (1) or Negative(0)
def _get_polarity(self):
_, value = self.query('GET SIGN')
if value == 'POSITIVE':
polarity = '+'
elif value == 'NEGATIVE': # assume Negative
polarity = '-'
return polarity
def _get_maxField(self): # Get the maximum B field, returns a float (in Amps or Tesla)
_, value = self.query('GET MAX')
units = self._get_unit()
if units == 1:
m = re.match(r'({}) TESLA'.format(
CryogenicSMS120C._re_float_exp), value)
elif units == 0:
m = re.match(r'({}) AMPS'.format(
CryogenicSMS120C._re_float_exp), value)
maxField = float(m[1])
return maxField
# Get current magnetic field, returns a float (if unit is Tesla, otherwise raises an exception)
def _get_field(self):
if self._get_unit() != 1:
raise Exception('Controller is not in TESLA mode, switch to TESLA to get the field')
_, value = self.query('GET OUTPUT')
m = re.match(r'({}) TESLA AT ({}) VOLTS'.format(CryogenicSMS120C._re_float_exp,CryogenicSMS120C._re_float_exp), value)
field = float(m[1])
return field
def _get_rampStatus(self): # get current magnet status, returns an integer
_, value = self.query('RAMP STATUS')
if 'HOLDING' in value: # holding on
rampStatus = 0
elif 'RAMPING' in value: # magnet ramping
rampStatus = 1
elif 'QUENCH' in value: # detect magnet quench
rampStatus = 2
elif 'EXTERNAL' in value: # detect external trip
rampStatus = 3
elif 'FAULT' in value: # detect either controller or power fault
rampStatus = 4
return rampStatus
# checks if controller is paused (1) or active (0), returns a boolean
# integer
def _get_pauseRamp(self):
_, value = self.query('PAUSE')
if value == 'ON':
pauseRamp = 1
else: # assume pause OFF
pauseRamp = 0
return pauseRamp
# Get current magnet ramping rate, returns a float (in units of Amps/sec
# only)
def _get_rampRate(self):
_, value = self.query('GET RATE')
m = re.match(
r'({}) A/SEC'.format(CryogenicSMS120C._re_float_exp), value)
rampRate = float(m[1])
return rampRate
# Set magnet sweep direction : "+" for positive B, "-" for negative B
def _set_polarity(self, val):
# using standard write as read returns an error/is non-existent.
if self._get_persistentMode() == False and abs(self._get_field()) <= 0.007:
self.write('DIRECTION %s' % val)
return True
elif self._get_persistentMode() == True:
log.error(
'Cannot switch polarity, magnet in persistent mode - please engage switch heater and go to zero field before changing sign.')
return False
elif abs(self._get_field()) > 0.007:
log.error('Recommended to switch sign only when field is at zero.')
return False
else:
log.error('Cannot switch polarity, check magnet.')
return False
def _set_unit(self, val): # Set unit to Tesla(1) or Amps(0),
# Enables us to set units of Tesla
self.ask('SET TPA %f' % self._coil_constant)
self.ask('TESLA %d' % val)
def _set_maxField(self, val): # Set the maximum field (in Amps or Tesla)
self.ask('SET MAX %0.2f' % val)
def _set_switchHeater(self, val): # Turn heater ON(1) or OFF(0)
if self._get_rampStatus() == 1:
log.error(
'Cannot switch heater during a ramp, first pause the controller.')
else:
# Switch ON, if currently OFF
if val == 1 and (self._get_switchHeater() == False):
strHeaterStatus = self.ask('HEATER %d' % val)
switchHeater = 1
# Switch OFF, if currently ON
elif val == 0 and (self._get_switchHeater() == True):
strHeaterStatus = self.ask('HEATER %d' % val)
switchHeater = 0
else: # assume no change to current switch heater state
strHeaterStatus = self.ask('HEATER %d' % val)
switchHeater = self._get_switchHeater()
log.info(strHeaterStatus)
return switchHeater
# Move into persistent mode (1) or out of persistent mode(0)
def _set_persistentMode(self, val):
if self._get_rampStatus() == 0: # Check magnet on HOLD
currField = self._get_field()
log.info("Leads now at %f ." % currField)
# Enter persistent mode from non-persistent
if val == 1:
if self._get_persistentMode() == False and (self._get_switchHeater() == True):
log.info('Moving into persistent mode:')
switchHeater = 0
strHeaterStatus = self.ask('HEATER %d' % switchHeater)
log.info(strHeaterStatus)
log.info('Waiting 60s for switch heater to cool down.')
time.sleep(60)
log.info('Ramping down magnet leads...')
self._set_field(0)
while True:
if self._get_rampStatus() == 0:
log.info('Leads at zero.')
persistentMode = 1
persistentField = self._get_persistentField()
log.info(
'Magnet is in persistent mode at Field = %f.' % persistentField)
break
time.sleep(5) # check every 5 seconds
elif self._get_persistentMode() == True:
persistentMode = 1
persistentField = self._get_persistentField()
log.info('Already in persistent mode.')
# Exit persistent mode
elif val == 0:
persistentField = self._get_persistentField()
if self._get_persistentMode() == True and (self._get_switchHeater() == False):
log.info('Exiting persistent mode from a field of %f' %
persistentField)
switchHeater = 1
strHeaterStatus = self.ask('HEATER %d' % switchHeater)
log.info(strHeaterStatus)
log.info('Waiting 30s for switch heater to warm up.')
time.sleep(30)
log.info(
'Matching magnet lead current to persistent field of %f...' % persistentField)
self._set_field(persistentField)
while True:
if self._get_rampStatus() == 0:
persistentMode = 0
persistentField = 0
log.info('Magnet is non-persistent.')
break
time.sleep(5) # check every 5 seconds
elif self._get_persistentMode() == False:
persistentMode = 0
persistentField = 0
log.info('Magnet already non-persistent.')
else:
log.warning(
'Cannot change (non-)persistent mode state, check magnet status.')
return persistentMode, persistentField
def _set_pauseRamp(self, val): # Pause magnet controller Pause=1, Unpause=0
self.ask('PAUSE %d' % val)
# Set ramp speed Amps/sec , check it is reasonable if it is being manually
# modified
def _set_rampRate(self, val):
if self._current_ramp_limit is None:
self._current_ramp_limit = CryogenicSMS120C.default_current_ramp_limit
if val <= self._current_ramp_limit:
self.ask('SET RAMP %0.2f' % val)
return True
elif val > CryogenicSMS120C.default_max_current_ramp_limit:
self.ask('SET RAMP %0.2f' %
CryogenicSMS120C.default_current_ramp_limit)
msg = 'Requested rate of {} is unsafe and over the maximum limit of {} A/s. Coerced to default ramp rate.'
log.error(msg.format(
val, CryogenicSMS120C.default_max_current_ramp_limit))
return False
else:
msg = 'Requested ramp speed is over the limit of {} A/s. Change limit, at your own risk after consulting the SMS120C manual.'
log.error(msg.format(self._current_ramp_limit))
return False
# Check magnet state and ramp speed to see if it is safe to ramp, returns
# boolean
def _can_startRamping(self):
state = self._get_rampStatus()
if self._get_rampRate() <= self._current_ramp_limit:
if state == 2: # Quench
log.error(
'Magnet quench detected - please check magnet status before ramping.')
return False
elif state == 1: # Ramping
log.info('Magnet currently ramping.')
return True
elif state == 0: # Holding
log.info('Magnet currently holding.')
return True
log.error(
'Could not ramp, magnet in state: {}'.format(state))
return False
else:
log.warning(
'Could not ramp, ramp rate is over the set limit, please lower.')
return False
# Between any two commands, there are must be around 200ms waiting time.
def _set_field(self, val):
if not self.switchHeater(): # If switch heater is OFF
log.error('Unable to set field, switch heater is off, persistent mode may be active')
return
# check ramp status is OK
if self._can_startRamping():
# Check that field is not outside max.field limit
if (self._get_unit() == 1 and (val <= self._get_maxField())) or (
self._get_unit() == 0 and (val <= self._current_rating)):
# pause the controller if it is currently ramping
self._set_pauseRamp(1)
self.ask('SET MID %0.2f' % val) # Set target field
self._set_pauseRamp(0) # Unpause the controller
# Ramp magnet/field to MID or ZERO (Note: Using standard write
# as read returns an error/is non-existent).
if val == 0:
self.write('RAMP ZERO')
log.info('Ramping magnetic field to zero...')
else:
self.write('RAMP MID')
log.info('Ramping magnetic field...')
else:
log.error(
'Target field is outside max. limits, please lower the target value.')
else:
log.error('Cannot set field - check magnet status.')
def _set_field_bidirectional(self, val):
polarity = self._get_polarity()
desired_polarity = '-' if val < 0 else '+'
if ((polarity == '+' and desired_polarity == '-') or
(polarity == '-' and desired_polarity == '+')):
self._set_field(0)
# This is, sadly, blocking
self._wait_for_field_zero(0)
self._set_polarity(desired_polarity)
self._set_field(abs(val))
def _wait_for_field_zero(self, field_threshold=0.003, refresh_time=0.1):
"""Waits for the field to be within a certain threshold"""
while abs(self.field()) > field_threshold:
time.sleep(refresh_time)