"""A software representation of the Oxford Heliox 3He insert."""
from threading import Lock
from time import sleep, time
from src.core import instrument as inst
from src.core.action import Action, ActionScan, ActionSpec, ParameterSpec
from src.instruments.noauto.itc503 import ITC503
from src.instruments.noauto.oxford_common import readAddressConfig
from src.instruments.noauto.oxford_valve import OxfordValve
from src.instruments.noauto.ips120 import IPS120
from src.tools import path_tools as pt
from src.tools import config_parser as cp
from src.tools.coordinates import cartesianToSpherical as c2s
from src.tools.coordinates import sphericalToCartesian as s2c
from src.tools.stability import StabilityTrend, StabilitySetpoint
from src.tools.general import simpleLinearRegression
MODE_DIRECT = 0
MODE_THROUGH_MONITOR = 1
[docs]class VectorMagnet(inst.Instrument):
"""A Vector Magnet.
Parameters
----------
experiment : Experiment
The experiment which owns the instrument.
name : str
The name of the instrument. Typically, it will always be 'Vector
Magnet'.
"""
def __init__(self, experiment, name='Vector Magnet', spec=None):
super(VectorMagnet, self).__init__(experiment, name, spec)
self._info = ('Instrument: ' + self.getName() + '\n' +
'Oxford Instruments Vector Magnet and Triton 3He System')
confFile = pt.unrel('config', 'vector_magnet.conf')
confmag1 = readAddressConfig(confFile, 'ps1_address')
confmag2 = readAddressConfig(confFile, 'ps2_address')
confmag3 = readAddressConfig(confFile, 'ps3_address')
self._powerSupplies = [IPS120(**confmag1),
IPS120(**confmag2),
IPS120(**confmag3)]
conftemp1 = readAddressConfig(confFile, 'tc1_address')
conftemp2 = readAddressConfig(confFile, 'tc2_address')
conftemp3 = readAddressConfig(confFile, 'tc3_address')
self._tempControllers = [ITC503(**conftemp1),
ITC503(**conftemp2),
ITC503(**conftemp3)]
confvalve = readAddressConfig(confFile, 'aux_address')
self._valve = OxfordValve(**confvalve)
conf = cp.ConfigParser(confFile, cp.FORMAT_AUTO)
self._heSorb = conf.getOptionsDict('he3_sorb')
self._heLow = conf.getOptionsDict('he3_pot_low')
self._heHigh = conf.getOptionsDict('he3_pot_high')
self._heatSwitch = conf.getOptionsDict('heat_switch')
self._pt1 = conf.getOptionsDict('pt1_plate')
self._pt2 = conf.getOptionsDict('pt2_plate')
self._int = conf.getOptionsDict('int_plate')
self._mag = conf.getOptionsDict('magnet')
for item in (self._heSorb, self._heLow, self._heHigh, self._heatSwitch,
self._pt1, self._pt2, self._int, self._mag):
item['itc'] = self._tempControllers[item['itc']]
self._ctrlTemp = conf.getOptionsDict('control_temp')
self._ctrlCool = conf.getOptionsDict('control_cooldown')
self._ctrlPrecon = conf.getOptionsDict('control_precondense')
self._ctrlCon = conf.getOptionsDict('control_condense')
self._ctrlRecon = conf.getOptionsDict('control_autorecondense')
self._temperatures = {}
# These are set at initialization time.
self._field = [0.0, 0.0, 0.0]
self._fieldSetpoint = [0.0, 0.0, 0.0]
self._rampLimits = [0.250, 0.125, 0.125]
self._rampProportion = 1.0
self._cartesian = True
self._mode = MODE_DIRECT
self._lock = Lock()
#===========================================================================
# General
#===========================================================================
[docs] def initialize(self):
"""Initialize the Oxford vector magnet system."""
for supply in self._powerSupplies:
supply.initialize()
[docs] def finalize(self):
"""Finalize the Oxford vector magnet system."""
for supply in self._powerSupplies:
supply.closeCommunication()
[docs] def setMode(self, newMode):
"""Set the vector magnet system reading mode.
Parameters
----------
newMode : int
An integer (MODE_DIRECT or MODE_THROUGH_MONITOR) to specify the
reading mode. If it is MODE_DIRECT, all data come directly from
the temperature controller. If it is MODE_THROUGH_MONITOR, only
the temperature monitor triggers direct readings from the
controller, and other requests from data receive the most recent
readings so triggered.
"""
self._mode = newMode
#===========================================================================
# Magnetic field
#===========================================================================
[docs] def setFieldNoWaitX(self, field):
"""Set the x-component of the magnetic field.
Parameters
----------
field : float
The new value of the x-component of the magnetic field in Tesla.
"""
with self._lock:
if not self._cartesian:
self._fieldSetpoint = s2c(*self._fieldSetpoint)
self._field = s2c(*self._field)
self._cartesian = True
self._fieldSetpoint[0] = field
self._powerSupplies[0].setSweepRate(self._rampLimits[0] *
self._rampProportion)
self._powerSupplies[0].setField(field)
[docs] def setFieldX(self, field, block):
"""Set the x-component of the magnetic field.
Parameters
----------
field : float
The new x-component for the magnetic field.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitX(field)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldCartesian)
self.directGetFieldCartesian()
[docs] def setFieldNoWaitY(self, field):
"""Set the y-component of the magnetic field.
Parameters
----------
field : float
The new value of the y-component of the magnetic field in Tesla.
"""
with self._lock:
if not self._cartesian:
self._fieldSetpoint = s2c(*self._fieldSetpoint)
self._field = s2c(*self._field)
self._cartesian = True
self._fieldSetpoint[1] = field
self._powerSupplies[1].setSweepRate(self._rampLimits[1] *
self._rampProportion)
self._powerSupplies[1].setField(field)
[docs] def setFieldY(self, field, block):
"""Set the y-component of the magnetic field.
Parameters
----------
field : float
The new x-component for the magnetic field.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitY(field)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldCartesian)
self.directGetFieldCartesian()
[docs] def setFieldNoWaitZ(self, field):
"""Set the z-component of the magnetic field.
Parameters
----------
field : float
The new value of the z-component of the magnetic field in Tesla.
"""
with self._lock:
if not self._cartesian:
self._fieldSetpoint = s2c(*self._fieldSetpoint)
self._field = s2c(*self._field)
self._cartesian = True
self._fieldSetpoint[2] = field
self._powerSupplies[0].setSweepRate(self._rampLimits[2] *
self._rampProportion)
self._powerSupplies[0].setField(field)
[docs] def setFieldZ(self, field, block):
"""Set the z-component of the magnetic field.
Parameters
----------
field : float
The new x-component for the magnetic field.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitZ(field)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldCartesian)
self.directGetFieldCartesian()
[docs] def setField(self, field, block='yes'):
"""Set the z-component of the magnetic field.
Parameters
----------
field : float
The desired z-component of the magnetic field in Tesla.
"""
self.setFieldZ(field, block)
[docs] def setFieldNoWaitMagnitude(self, field):
"""Set the magnitude of the magnetic field.
Parameters
----------
field : float
The new magnitude for the magnetic field in Tesla.
"""
with self._lock:
if self._cartesian:
self._field = c2s(*self._field)
self._fieldSetpoint = c2s(*self._fieldSetpoint)
self._cartesian = False
self._fieldSetpoint[0] = field
self._setSphericalFieldNoLock()
[docs] def setFieldMagnitude(self, field, block):
"""Set the magnitude of the magnetic field.
Parameters
----------
field : float
The new magnitude for the magnetic field in Tesla.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitMagnitude(field)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldSpherical)
self.directGetFieldSpherical()
[docs] def setFieldNoWaitAzimuthal(self, azimuthalAngle):
"""Set the azimuthal angle of the magnetic field.
Parameters
----------
azimuthalAngle : float
The desired azimuthal angle for the magnetic field, measured in
degrees down from the positive z-axis.
"""
with self._lock:
if self._cartesian:
self._field = c2s(*self._field)
self._fieldSetpoint = c2s(*self._fieldSetpoint)
self._cartesian = False
self._fieldSetpoint[1] = azimuthalAngle
self._setSphericalFieldNoLock()
[docs] def setFieldAzimuthal(self, azimuthalAngle, block):
"""Set the magnitude of the magnetic field.
Parameters
----------
azimuthalAngle : float
The new azimuthal angle for the field vector, measured in degrees
down from the positive z-axis.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitAzimuthal(azimuthalAngle)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldSpherical)
self.directGetFieldSpherical()
[docs] def setFieldNoWaitPolar(self, polarAngle):
"""Set the polar angle of the magnetic field.
Parameters
----------
polarAngle : float
The desired polar angle for the magnetic field, measured in
degrees counter-clockwise from the positive x-axis.
"""
with self._lock:
if self._cartesian:
self._field = c2s(*self._field)
self._fieldSetpoint = c2s(*self._fieldSetpoint)
self._cartesian = False
self._fieldSetpoint[1] = polarAngle
self._setSphericalFieldNoLock()
[docs] def setFieldPolar(self, polarAngle, block):
"""Set the magnitude of the magnetic field.
Parameters
----------
polarAngle : float
The new polar angle for the field vector, measured in degrees
counter-clockwise from the positive x-axis.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitAzimuthal(polarAngle)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldSpherical)
self.directGetFieldSpherical()
[docs] def setFieldNoWaitCartesian(self, fieldX, fieldY, fieldZ):
"""Set the magnetic field to a specified value in Cartesian coordinates.
Parameters
----------
fieldX : float
The x-component of the field in Tesla.
fieldY : float
The y-component of the field in Tesla.
fieldZ : float
The z-component of the field in Tesla.
"""
with self._lock:
if not self._cartesian:
self._field = s2c(*self._field)
self._cartesian = True
self._fieldSetpoint = [fieldX, fieldY, fieldZ]
rates = self._calculateSweepRate(self._field, self._fieldSetpoint)
for supply, rate, target in zip(self._powerSupplies, rates,
self._fieldSetpoint):
supply.setSweepRate(rate)
supply.setField(target)
[docs] def setFieldCartesian(self, fieldX, fieldY, fieldZ, block):
"""Set the magnetic field to a specified value in Cartesian coordinates.
Parameters
----------
fieldX : float
The x-component of the field in Tesla.
fieldY : float
The y-component of the field in Tesla.
fieldZ : float
The z-component of the field in Tesla.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitCartesian(fieldX, fieldY, fieldZ)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldCartesian)
self.directGetFieldSpherical()
[docs] def setFieldNoWaitSpherical(self, magnitude, azimuthalAngle, polarAngle):
"""Set the magnetic field in spherical coordinates.
Parameters
----------
magnitude : float
The magnitude of the magnetic field in Tesla.
azimuthalAngle : float
The desired azimuthal angle, measured in degrees downward from the
positive z-axis.
polarAngle : float
The desired polar angle, measured in degrees counter-clockwise from
the positive x-axis.
"""
with self._lock:
if self._cartesian:
self._cartesian = False
self._field = c2s(*self._field)
self._fieldSetpoint = [magnitude, azimuthalAngle, polarAngle]
self._setSphericalFieldNoLock()
[docs] def setFieldSpherical(self, magnitude, azimuthalAngle, polarAngle, block):
"""Set the magnetic field in spherical coordinates.
Parameters
----------
magnitude : float
The magnitude of the magnetic field in Tesla.
azimuthalAngle : float
The desired azimuthal angle, measured in degrees downward from the
positive z-axis.
polarAngle : float
The desired polar angle, measured in degrees counter-clockwise from
the positive x-axis.
block : str
A string, either 'Yes' or 'No', indicating whether to block the
sequence until the desired field is reached.
"""
self.setFieldNoWaitSpherical(magnitude, azimuthalAngle, polarAngle)
if block.lower() == 'yes':
self.waitForField(self.directGetFieldSpherical)
self.directGetFieldSpherical()
def _setSphericalFieldNoLock(self):
"""Command the power supplies to ramp to the spherical setpoints.
Assume that the current field and setpoint are in spherical coordinates
and convert to Cartesian in local variables (i.e., without changing any
instance attributes), calculate the appropriate ramp rates,
set the ramp rates, and command the supplies to proceed.
"""
oldField = s2c(*self._field)
newField = s2c(*self._fieldSetpoint)
ramps = self._calculateSweepRate(oldField, newField)
for supply, ramp, field in zip(self._powerSupplies, ramps, newField):
supply.setSweepRate(ramp)
supply.setField(field)
def _calculateSweepRate(self, oldField, newField):
"""Determine the sweep rates to go from one field to another.
Parameters
----------
oldField : list of float
The old Cartesian field components, in Tesla.
newField : list of float
The new Cartesian field components, in Tesla.
Returns
-------
list of float
The field sweep rates for the three power supplies in Tesla/min.
"""
differences = []
rampTimes = []
for oldComp, newComp, maxRamp in zip(oldField, newField,
self._rampLimits):
fieldDiff = abs(newComp - oldComp)
rampTimes.append(fieldDiff / (self._rampProportion * maxRamp))
differences.append(abs(newComp - oldComp))
rampTime = max(rampTimes)
realRates = []
for diff in differences:
realRates.append(diff / rampTime)
return realRates
[docs] def pauseField(self):
"""Pause the field sweep."""
for supply in self._powerSupplies:
supply.setActivity('0')
[docs] def unpauseField(self):
"""Resume the field sweep."""
for supply in self._powerSupplies:
supply.setActivity('1')
[docs] def isFieldAtSetpoint(self):
"""Return whether the fields have reached the setpoints."""
answer = True
for field, setpoint in zip(self._field, self._fieldSetpoint):
if abs(field - setpoint) > 0.00001:
answer = False
return answer
[docs] def waitForField(self, readMethod):
"""Wait until the field has reached its target.
Parameters
----------
readMethod : instancemethod
The method to use to update information about the current fields.
It should probably be either `directGetFieldCartesian` or
`directGetFieldSpherical`.
"""
while not self.isFieldAtSetpoint():
readMethod()
sleep(0.2)
if self._expt.isPaused():
self.pauseField()
while self._expt.isPaused():
sleep(0.2)
self.unpauseField()
[docs] def directGetFieldCartesian(self):
"""Get the magnetic field in Cartesian coordinates.
Returns
-------
float
The x-component of the magnetic field.
float
The y-component of the magnetic field.
float
The z-component of the magnetic field.
"""
with self._lock:
newX = self._powerSupplies[0].getField()
newY = self._powerSupplies[1].getField()
newZ = self._powerSupplies[2].getField()
if self._cartesian:
self._field = [newX, newY, newZ]
return tuple(self._field)
else:
self._field = c2s(newX, newY, newZ, self._fieldSetpoint[0] < 0)
return (newX, newY, newZ)
[docs] def getFieldCartesian(self):
"""Get the field vector in Cartesian coordinates.
Returns
-------
float
The x-component of the magnetic field vector.
float
The y-component of the magnetic field vector.
float
The z-component of the magnetic field vector.
"""
if self._mode == MODE_DIRECT:
return self.directGetFieldCartesian()
elif self._cartesian:
return tuple(self._field)
return tuple(s2c(*self._field))
[docs] def getField(self):
"""Get the z-component of the magnetic field.
Returns
-------
float
The z-component of the magnetic field in Tesla.
"""
return self.getFieldCartesian()[2]
[docs] def directGetFieldSpherical(self):
"""Get the magnetic field in Cartesian coordinates.
Returns
-------
float
The magnitude of the magnetic field in Tesla.
float
The azimuthal angle of the magnetic field vector, measured in
degrees down from the positive z-axis.
float
The polar angle of the magnetic field vector, measured in
degrees counter-clockwise from the positive x-axis.
"""
with self._lock:
newX = self._powerSupplies[0].getField()
newY = self._powerSupplies[1].getField()
newZ = self._powerSupplies[2].getField()
if self._cartesian:
self._field = [newX, newY, newZ]
return tuple(c2s(newX, newY, newZ, self._fieldSetpoint[0] < 0))
else:
self._field = c2s(newX, newY, newZ, self._fieldSetpoint[0] < 0)
return tuple(self._field)
[docs] def getFieldSpherical(self):
"""Get the field vector in spherical coordinates.
Returns
-------
float
The magnitude of the magnetic field vector.
float
The azimuthal angle of the magnetic field vector, measured in
degrees down from the positive z-axis.
float
The polar angle of the magnetic field vector, measured in degrees
counter-clockwise from the positive x-axis.
"""
if self._mode == MODE_DIRECT:
return self.directGetFieldSpherical()
elif self._cartesian:
return tuple(c2s(self._field[0], self._field[1], self._field[2],
self._fieldSetpoint[0] < 0))
return tuple(self._field)
[docs] def directGetFieldSetpoints(self):
"""Read the field setpoints from the power supplies.
Returns
-------
float
The magnetic field setpoint in the x-direction in Tesla.
float
The magnetic field setpoint in the y-direction in Tesla.
float
The magnetic field setpoint in the z-direction in Tesla.
"""
with self._lock:
setpointX = self._powerSupplies[0].getFieldSetpoint()
setpointY = self._powerSupplies[1].getFieldSetpoint()
setpointZ = self._powerSupplies[2].getFieldSetpoint()
if self._cartesian:
self._fieldSetpoint = [setpointX, setpointY, setpointZ]
else:
self._fieldSetpoint = c2s(setpointX, setpointY, setpointZ,
self._fieldSetpoint[0] < 0)
return (setpointX, setpointY, setpointZ)
[docs] def getFieldSetpoints(self):
"""Get the magnetic field setpoints.
Returns
-------
float
The magnetic field setpoint in the x-direction in Tesla.
float
The magnetic field setpoint in the y-direction in Tesla.
float
The magnetic field setpoint in the z-direction in Tesla.
"""
if self._mode == MODE_DIRECT:
return self.directGetFieldSetpoints()
elif self._cartesian:
return tuple(self._fieldSetpoint)
return tuple(s2c(*self._fieldSetpoint))
[docs] def setFieldRampProportion(self, proportion):
"""Set the magnetic field ramp rate proportion.
Parameters
----------
proportion : float
The ratio of the desired ramp rate to the maximum ramp rate. The
actual rate used for any given magnet sweep will be such that all
power supplies reach the target at the same time, limited by the
power supply with the lowest maximum ramp rate, which will be
multiplied by `proportion`.
"""
self._rampProportion = proportion
[docs] def getFieldRampProportion(self):
"""Get the magnetic field ramp rate proportion.
Returns
-------
float
The ratio of the desired ramp rate to the maximum ramp rate. The
actual rate used for any given magnet sweep will be such that all
power supplies reach the target at the same time, limited by the
power supply with the lowest maximum ramp rate, which will be
multiplied by `proportion`.
"""
return self._rampProportion
[docs] def directGetFieldRampRates(self):
"""Read the magnetic field sweep rates directly from the power supplies.
Returns
-------
float
The ramp rate for the x-component of the magnetic field in
Tesla/min.
float
The ramp rate for the y-component of the magnetic field in
Tesla/min.
float
The ramp rate for the z-component of the magnetic field in
Tesla/min.
"""
ans = []
for supply in self._powerSupplies:
ans.append(supply.getSweepRate())
return tuple(ans)
[docs] def getFieldRampRates(self):
"""Get the magnetic field ramp rates.
Returns
-------
float
The ramp rate for the x-component of the magnetic field in
Tesla/min.
float
The ramp rate for the y-component of the magnetic field in
Tesla/min.
float
The ramp rate for the z-component of the magnetic field in
Tesla/min.
"""
if self._mode == MODE_DIRECT:
return self.directGetFieldRampRates()
answer = []
for rate in self._rampLimits:
answer.append(self._rampProportion * rate)
return tuple(answer)
#===========================================================================
# Temperature
#===========================================================================
def _auxReadTemp(self, sensorData):
"""Return the temperature measured by the specified sensor.
Acquire the lock, read the temperature from the relevant controller,
update the temperature in the vector magnet's dictionary, and return
the temperature.
Parameters
----------
sensorData : dict
A dictionary indicating the sensor from which to read. It must have
an `ITC503` object under the heading 'itc' and a sensor index
string ('1', '2', or '3') under the key 'sensor'.
Returns
-------
float
The temperature measured by the specified sensor in Kelvin.
"""
with self._lock:
temp = sensorData['itc'].getTemperature(sensorData['sensor'])
self._temperatures['label'] = temp
return temp
def _auxReadSetpointAndPID(self, tempController):
"""Return the setpoint and PID values for the temperature controller.
Acquire the lock and read the setpoint and the PID values from the
ITC.
Parameters
----------
tempController : ITC503
The Oxford ITC 503 from which to read the requested data.
Returns
-------
float
The setpoint for the active sensor on the controller.
float
The proportional band value for the controller.
float
The integral action time for the controller.
float
The derivative action time for the controller.
"""
with self._lock:
setpoint = tempController.getSetpoint()
pid = tempController.getPID()
return (setpoint, pid[0], pid[1], pid[2])
[docs] def directGetTemperatureHe3(self):
"""Read the He3 pot temperature from the temperature controller.
Returns
-------
float
The temperature of the He3 pot.
"""
maxLowTemp = self._heLow['max_temp']
lowTemp = self._auxReadTemp(self._heLow)
if lowTemp <= maxLowTemp:
self._temperatures['He3 Pot'] = lowTemp
return lowTemp
highTemp = self._auxReadTemp(self._heHigh)
self._temperatures['He3 Pot'] = highTemp
return highTemp
[docs] def directGetTemperatureSorb(self):
"""Read the sorb temperature from the temperature controller.
Returns
-------
float
The temperature of the sorb.
"""
return self._auxReadTemp(self._heSorb)
[docs] def directGetTemperatureHeatSwitch(self):
"""Read the heat switch temperature from the temperature controller.
Returns
-------
float
The temperature of the heat switch.
"""
return self._auxReadTemp(self._heatSwitch)
[docs] def directGetTemperaturePT1Plate(self):
"""Read the PT 1 plate temperature from the temperature controller.
Returns
-------
float
The temperature of the PT1 plate.
"""
return self._auxReadTemp(self._pt1)
[docs] def directGetTemperaturePT2Plate(self):
"""Read the PT 2 plate temperature from the temperature controller.
Returns
-------
float
The temperature of PT 2 plate.
"""
return self._auxReadTemp(self._pt2)
[docs] def directGetTemperatureIntPlate(self):
"""Read the intermediate plate temperature from the controller.
Returns
-------
float
The temperature of the intermediate plate.
"""
return self._auxReadTemp(self._int)
[docs] def directGetTemperatureMagnet(self):
"""Read the magnet temperature from the temperature controller.
Returns
-------
float
The temperature of the magnet plate.
"""
return self._auxReadTemp(self._mag)
[docs] def directGetHe3SetpointAndPid(self):
"""Read the He3 temperature setpoint and PID values from the controller.
Returns
-------
float
The setpoint for the He3 pot.
float
The proportional band value for the controller.
float
The integral action time for the controller.
float
The derivative action time for the controller.
"""
low = self._heLow
high = self._heHigh
if (low['itc'].getHeaterSensor() == low['sensor'] and
low['itc'].getAutoStatus()[0]):
return self._auxReadSetpointAndPID(low['itc'])
elif (high['itc'].getHeaterSensor() == high['sensor'] and
high['itc'].getAutoStatus()[0]):
return self._auxReadSetpointAndPID(high['itc'])
return (0.0, 0.0, 0.0, 0.0)
[docs] def procedureCooldown(self):
"""Perform the system initial cooldown sequence."""
# Turn off power to all heaters
with self._lock:
for sensor in (self._heSorb, self._heHigh, self._heLow, self._pt1,
self._heatSwitch):
sensor['itc'].setAutoStatus(False, False)
sensor['itc'].setTemperature(0.0)
sensor['itc'].setHeaterOutput(0.0)
# Pre-cool: PT2 heater on, Valve V1 open
with self._lock:
_auxToggleHeater(self._pt2, True)
self._valve.openValve()
# Pre-cool: Wait for final He3 temp
targetTemp = self._ctrlCool['precool_final_he3_temp']
currentTemp = self._auxReadTemp(self._heHigh)
while targetTemp <= currentTemp:
currentTemp = self._auxReadTemp(self._heHigh)
sleep(0.5)
# Pre-cool: PT2 heater off
with self._lock:
_auxToggleHeater(self._pt2, False)
# Open V1, close heat switch
with self._lock:
_auxToggleHeater(self._heatSwitch, True)
self._valve.openValve()
# Turn on compressor
# FIXME: Send a message
# Wait for He3 to stabilize with sorb < target
sorbTarget = self._ctrlCool['sorb_target']
timer = StabilityTrend(120, self._ctrlCool['he3_stability_initial'])
while not (timer.isFinished() and
self.directGetTemperatureSorb() < sorbTarget):
timer.addPoint(self.directGetTemperatureHe3())
sleep(0.5)
del timer
# Close V1
with self._lock:
self._valve.closeValve()
# Open heat switch
with self._lock:
_auxToggleHeater(self._heatSwitch, False)
# Wait for heat switch to open
targetTemp = self._heatSwitch['off_temp']
currentTemp = self.directGetTemperatureHeatSwitch()
while currentTemp >= targetTemp:
currentTemp = self.directGetTemperatureHeatSwitch()
sleep(0.5)
# Ramp sorb to condense temperature
with self._lock:
_auxSetSetpointAndPID(self._heSorb,
self._ctrlCon['sorb_setpoint'])
# Wait for the He3 pot to start cooling
self._waitForHe3PotToStartCooling()
# Wait for He3 pot to get below 5K
currTemp = self.directGetTemperatureHe3()
while currTemp >= 5.0:
currTemp = self.directGetTemperatureHe3()
sleep(0.5)
def _waitForHe3PotToStartCooling(self):
"""Wait for the He3 pot to start cooling."""
startTime = downTime = currTime = time()
timeout = 1800.0
duration = 120.0
times = []
vals = []
while currTime - downTime < duration and currTime - startTime < timeout:
currTime = time()
times.append(currTime)
currTemp = self.directGetTemperatureHe3()
vals.append(currTemp)
if simpleLinearRegression(times, vals)[0] > -0.00001:
downTime = currTime
times = [currTime]
vals = [currTemp]
sleep(1.0)
[docs] def procedurePrecondense(self):
"""Prepare to condense the helium."""
# Turn the sorb off, turn the heat switch on, and open V1
with self._lock:
self._valve.openValve()
_auxSetSetpointAndPID(self._heSorb, 0.0, False, False)
_auxToggleHeater(self._heatSwitch, True)
# Delay
delay = 600.0
startTime = currTime = time()
while currTime - startTime < delay:
currTime = time()
sleep(1.0)
# Wait for the sorb to fall below its target
target = self._ctrlPrecon['sorb_target']
currTemp = self.directGetTemperatureSorb()
while currTemp > target:
currTemp = self.directGetTemperatureSorb()
sleep(0.5)
# Delay
delay = self._ctrlPrecon['delay']
startTime = currTime = time()
while currTime - startTime < delay:
currTime = time()
sleep(1.0)
# Close V1
with self._lock:
self._valve.closeValve()
[docs] def procedureCondense(self):
"""Condense the He3."""
# Close V1, turn off heat switch, and turn off sorb power
with self._lock:
self._valve.closeValve()
_auxToggleHeater(self._heatSwitch, False)
_auxSetSetpointAndPID(self._heSorb, 0.0, False, False)
# Wait for heat switch to turn off
tempOff = self._heatSwitch['off_temp']
currTemp = self.directGetTemperatureHeatSwitch()
while currTemp >= tempOff:
currTemp = self.directGetTemperatureHeatSwitch()
sleep(0.5)
# Warm sorb to intermediate temperature
sweepStart = self._ctrlCon['sorb_sweep_start']
with self._lock:
_auxSetSetpointAndPID(self._heSorb, sweepStart)
# Delay
_generalDelay(1200.0)
# Warm sorb to final sweep temperature
self._condenseWarmSorbToFinalRampTemp()
# Warm sorb to final condense temp
with self._lock:
_auxSetSetpointAndPID(self._heSorb,
self._ctrlCon['sorb_setpoint'])
# Delay
_generalDelay(1200.0)
# Wait for He3 pot to start cooling, waiting at least 3 min
minTime = 180.0
startTime = currTime = time()
timer = StabilityTrend(120, 0.0)
while (currTime - startTime < minTime or not timer.isBufferFull() or
timer.getTrend() > 0.0):
newTemp = self.directGetTemperatureHe3()
timer.addPoint(newTemp)
sleep(1.0)
del timer
# Wait for He3 to stabilize
minTime = 600.0
stability = self._ctrlCon['he3_stability']
absStability = abs(stability)
startTime = currTime = time()
timer = StabilityTrend(120, stability)
finished = False
while not finished:
runTime = currTime - startTime
newValue = self.directGetTemperatureHe3()
timer.addPoint(newValue)
slope = timer.getTrend()
if (runTime >= minTime and slope <= 0 and
abs(slope * 60.0) < absStability and timer.isStable()):
finished = True
sleep(1.0)
del timer
# Delay
_generalDelay(self._ctrlCon['delay'])
# Turn off sorb heater
with self._lock:
_auxSetSetpointAndPID(self._heSorb, 0.0, False, False)
# Wait for 1 min
sleep(60.0)
# Open valve V1
with self._lock:
self._valve.openValve()
# Wait for some time after valve opened
_generalDelay(self._ctrlCon['v1_open_time'])
# Close V1, close heat switch
with self._lock:
self._valve.closeValve()
_auxToggleHeater(self._heatSwitch, True)
def _condenseWarmSorbToFinalRampTemp(self):
"""Warm the sorb to its final condensation temperature."""
sweep = self._heSorb['sweep']
finalTemp = self._ctrlCon['sorb_sweep_end']
if sweep:
startTemp = self.directGetTemperatureSorb()
sweepRate = self._heSorb['sweep_rate'] / 60.0
sweepRate = abs(sweepRate) * ((finalTemp - startTemp) /
abs(finalTemp - startTemp))
finished = False
startTime = time()
while not finished:
currTime = time()
nextTemp = startTemp + (currTime - startTime) * sweepRate
if finalTemp > startTemp and nextTemp > finalTemp:
nextTemp = finalTemp
finished = True
elif finalTemp < startTemp and nextTemp < finalTemp:
nextTemp = finalTemp
finished = True
with self._lock:
_auxSetSetpointAndPID(self._heSorb, nextTemp)
sleep(0.25)
else:
with self._lock:
_auxSetSetpointAndPID(self._heSorb, finalTemp)
[docs] def procedureRecondense(self):
"""Recondense the cryostat."""
self.procedurePrecondense()
self.procedureCondense()
[docs] def procedureSetTemp(self, target):
"""Enter a temperature setpoint.
Parameters
----------
target : float
The desired temperature for the He3 pot in Kelvin.
"""
upperTemp = self._ctrlTemp['he3_upper_temp']
check = upperTemp > target
with self._lock:
if check:
self._valve.closeValve()
if check and self._ctrlTemp['he3_low_lim_low_hs_tset'] < target:
_auxSetSetpointAndPID(self._heatSwitch,
self._ctrlTemp['low_hs_tset'])
else:
_auxToggleHeater(self._heatSwitch, check)
if check:
ctrl = self._heLow
else:
ctrl = self._heHigh
_auxSetSetpointAndPID(ctrl, target, target < 1e-6, True, True)
[docs] def procedureRunToTemp(self, target):
"""Run the cryostat to the desired temperature.
Parameters
----------
target : float
The desired temperature for the He3 pot in Kelvin.
"""
# Enter the setpoint
self.procedureSetTemp(target)
# Wait for stability
if target < 1E-5:
timer = StabilityTrend(180, 0.005, 115200.0)
while not timer.isFinished():
newTemp = self.directGetTemperatureHe3()
timer.addPoint(newTemp)
sleep(1.0)
else:
stabilityTable = self._ctrlTemp['stability_table']
maxDeviation = searchStabilityTable(target, stabilityTable)
timer = StabilitySetpoint(180, target, maxDeviation, 7200.0)
while not timer.isFinished():
newTemp = self.directGetTemperatureHe3()
timer.addPoint(newTemp)
sleep(1.0)
del timer
# Delay
delay = self._ctrlTemp['delay_before_stable']
startTime = currTime = time()
while currTime - startTime < delay:
currTime = time()
sleep(1.0)
[docs] def getTemperature(self):
"""Return the temperature of the He3 pot.
Returns
-------
float
The temperature of the He3 pot in Kelvin.
"""
return self.directGetTemperatureHe3()
[docs] def setTemperature(self, temp):
"""Set the temperature for the He3 pot.
Parameters
----------
temp : float
The desired temperature for the He3 pot in Kelvin.
"""
currTemp = self.directGetTemperatureHe3()
cutoff = self._ctrlTemp['he3_upper_temp']
if temp < currTemp - 25:
self.procedureCooldown()
if temp < currTemp and currTemp > cutoff and temp < cutoff:
self.procedureRecondense()
self.procedureRunToTemp(temp)
[docs] def getActions(self):
"""Return the list of supported actions."""
return [
ActionSpec('get_field', Action,
{'experiment': self._expt,
'instrument': self,
'description': 'Get field',
'outputs': [
ParameterSpec('field',
{'experiment': self._expt,
'description': 'Magnetic field',
'formatString': '%.4f',
'binName': 'Field',
'binType': 'column'})
],
'string': 'Read the magnetic field.',
'method': self.getField}
),
ActionSpec('set_field', Action,
{'experiment': self._expt,
'instrument': self,
'description': 'Set field',
'inputs': [
ParameterSpec('field',
{'experiment': self._expt,
'description': 'Magnetic field',
'formatString': '%.4f',
'binName': 'Field',
'binType': 'column'}
),
ParameterSpec('wait',
{'experiment': self._expt,
'description': 'Following action',
'formatString': '%s',
'binName': None,
'binType': None,
'allowed': ['wait', 'proceed'],
'value': 'wait'}
)
],
'string': 'Set the magnetic field to $field T and $wait.',
'method': self.setField}
),
ActionSpec('scan_field', ActionScan,
{'experiment': self._expt,
'instrument': self,
'description': 'Scan field',
'inputs': [
ParameterSpec('field',
{'experiment': self._expt,
'description': 'Magnetic field',
'formatString': '%.4f[]',
'binName': 'Field',
'binType': 'column',
'value': [(0.0, 0.0, 0.0)]}
)
],
'string': 'Scan the magnetic field',
'method': self.setField}
)
]
#===========================================================================
# Class methods
#===========================================================================
@classmethod
def getRequiredParameters(cls):
return []
@classmethod
[docs] def isSingleton(cls):
"""Return whether at most one instance of the instrument may exist.
Returns
-------
bool
Whether only zero or one instance of the instrument may exist.
"""
return True
def _auxToggleHeater(dev, heaterOn=True):
"""Turn the PT2 or heat switch heater on or off.
Note that if the heater is off, this creates an "open thermal circuit",
so there is no thermal contact.
This method does **not** acquire the lock.
Parameters
----------
dev : dict
A dictionary of configuration parameters for the heater to toggle.
It should probably be either _pt2 or _heatSwitch.
heaterOn : bool
Whether the heater should be turned on. If `False`, the heater
will be turned off.
"""
_auxSetSetpointAndPID(dev, dev['setpoint_on'], heaterOn, False)
def _auxSetSetpointAndPID(dev, setpoint, heaterOn=True,
checkAutoPID=True, forcePID=False):
"""Set the temperature setpoint and PID values.
Set the setpoint and the PID values for the sensor specified by
`dev`, which should be a dictionary which specifies the ITC503
to use, the channel of the appropriate sensor, and either individual
PID values (i.e. the keys 'p', 'i', and 'd') or a table of PID values
specified as a list of tuples, where the elements of each tuple are,
in order, the upper temperature bound for the specified sensor, P, I,
and D.
This method does **not** acquire the lock.
Parameters
----------
dev : dict
The sensor configuration dictionary.
setpoint : float
The desired temperature setpoint in Kelvin.
heaterOn : bool
Whether to turn the heater on. If `False`, the heater will be
turned off. It should be set to `False` only for items which
control a thermal link (e.g. the PT2 plate and the switch heater),
and then only if the heater is to be turned off, causing thermal
isolation. The default is `True`.
checkAutoPID : bool
Whether to make sure auto-PID is disabled before setting the PID
values. The default is `True`.
forcePID : bool
Whether the PID values should be set regardless of the value of
`heaterOn`.
"""
devitc = dev['itc']
if 'pid_table' in dev:
foundPID, newPID = searchPIDTable(setpoint, dev['pid_table'])
else:
try:
newPID = (dev['p'], dev['i'], dev['d'])
foundPID = True
except KeyError:
newPID = (0, 0, 0)
foundPID = False
if checkAutoPID and devitc.getAutoPID():
foundPID = False
channelChanged = False
if not (devitc.getAutoStatus()[0] and
devitc.getHeaterSensor() == dev['sensor']):
devitc.setAutoStatus(False, False)
devitc.setHeaterSensor(dev['sensor'])
if 'heater_limit' in dev:
devitc.setMaximumHeaterVoltage(dev['heater_limit'])
channelChanged = True
if heaterOn:
devitc.setTemperature(setpoint)
if foundPID:
devitc.setPID(*newPID)
if channelChanged:
devitc.setAutoStatus(True, False)
elif forcePID and foundPID:
devitc.setPID(*newPID)
else:
devitc.setAutoStatus(False, False)
devitc.setHeaterOutput(0.0)
devitc.setTemperature(0.0)
def _generalDelay(delayTime, sleepTime=1.0):
"""Wait for a specified amount of time.
Parameters
----------
delayTime : float
The total time to wait, in seconds.
sleepTime : float
The time to sleep between checks, in seconds. This is so that
(1) the software can periodically update status information and (2)
the software can respond to user-generated "skip" events.
"""
startTime = currTime = time()
while currTime - startTime < delayTime:
currTime = time()
sleep(sleepTime)
[docs]def searchPIDTable(targetTemp, pidTable):
"""Return the PID values appropriate for a specified setpoint.
Parameters
----------
targetTemp : float
The setpoint for which PID values are desired.
pidTable : list of tuple of float
The PID table for the appropriate sensor. Each tuple should consist
of four values: the largest temperature for which the row applies
and the values of the P, I, and D control terms.
Returns
-------
bool
Whether the supplied PID table contains a row suitable for the
specified setpoint.
tuple of float
The PID values appropriate for the specified setpoint if such values
are contained in the table. Otherwise, the PID values associated
with the largest upper-bound temperature in the table.
"""
for upper, pVal, iVal, dVal in pidTable:
if upper >= targetTemp:
return (True, (pVal, iVal, dVal))
return (False, tuple(pidTable[-1][1:]))
[docs]def searchStabilityTable(targetTemp, stabilityTable):
"""Return the allowed deviation for the specified setpoint.
Parameters
----------
targetTemp : float
The desired temperature setpoint in Kelvin.
stabilityTable : list of tuple
A list of tuples, where each tuple contains three elements: a float
indicating the maximum temperature for which the row is applicable,
a float indicating the maximum deviation, and a string indicating how
the maximum deviation should be interpreted. If the string is 'value',
the deviation will be interpreted as an absolute value; otherwise, it
will be interpreted as a fraction of the setpoint.
Returns
-------
float
The maximum deviation from the setpoint a system can exhibit while
still be considered stable.
"""
ans = stabilityTable[-1][1:]
for upper, dev, kind in stabilityTable:
if upper >= targetTemp:
ans = [dev, kind]
dev, kind = ans
if kind == 'value':
return dev
return dev * targetTemp