Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cryomagnetics vector magnet power supply hardware #636

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions documentation/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ please use _ni_x_series_in_streamer.py_ as hardware module.
* Added a config option to regulate pid logic timestep length
* New SwitchInterface and updated logic plus GUI
* Added biexponential fit function, model and estimator
* Added a hardware module to interface the Cryomagnetics super conducting magnet power supply


Config changes:
Expand Down
173 changes: 173 additions & 0 deletions hardware/sc_magnet/cryomagnetics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
"""
Hardware file for the Cryomagnetics power supply for superconducting magnet

QuDi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

QuDi is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with QuDi. If not, see <http://www.gnu.org/licenses/>.

Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/>
"""


import visa
from core.module import Base
from core.configoption import ConfigOption


class Cryomagnetics(Base):
""" Hardware module to control one or two vector magnet via the power supply.

This hardware works by setting a lower and a higher limit. Then the sweep operation can be used to either go
to lower limit, zero or upper limit.
A security constraints can be set via panel but is independent of the limits set here.

Cryomagnetics hardware use only integers of Gauss as setpoint. To have the best possible resolution, it's best to
use ampere and set the field_to_current_ratio via this hardware.

Example config for copy-paste:

cryognatics_xy:
module.Class: 'sc_magnet.cryomagnetics.Cryomagnetics'
visa_address: 'tcpip0::192.168.0.254:4444:socket'
limits: [-0.5, 0.5]

"""
_visa_address = ConfigOption('visa_address', missing='error')
_dual_supply = ConfigOption('dual_supply', False)
_limits = ConfigOption('limits', missing='error') # limits of field in Tesla. Ex: [-0.5, 0.5]
_field_to_current_ratio = ConfigOption('field_to_current_ratio', [71.0, 71.0]) # in G/A

def __init__(self, **kwargs):
"""Here the connections to the power supplies and to the counter are established"""
super().__init__(**kwargs)
self._inst = None

def on_activate(self):
""" Connect to hardware """

rm = visa.ResourceManager()
try:
self._inst = rm.open_resource(str(self._visa_address), write_termination='\r\n',
read_termination='\r\n')
except visa.VisaIOError:
self.log.error('Could not connect to hardware. Please check the wires and the address.')

def on_deactivate(self):
""" Disconnect from hardware """
self._inst.close()

def _query(self, command, channel=None):
""" Query a command to the hardware """
if channel in [1, 2]:
command = 'CHAN {};{}'.format(channel, command)
return self._inst.query(command)

def _write(self, command, channel=None):
""" Write a command to the hardware """
if channel in [1, 2]:
command = 'CHAN {};{}'.format(channel, command)
self._inst.write(command)

def get_channels(self):
""" Return a list of the channels keys """
return 1, 2

def set_channel(self, channel):
""" Set the current active channel """
if channel in [1, 2]:
self._write('CHAN {}'.format(channel))

def _to_tesla(self, value_as_text, channel=None):
""" Convert a return field to tesla """
if 'kG' in value_as_text:
self.log.warning('Cryomagnetics hardware use only integers of Gauss. Use Amperes for best resolution.')
value = float(value_as_text[:-2]) # in kG
value *= 0.1 # in Tesla
elif 'A' in value_as_text:
value = float(value_as_text[:-1]) # in Ampere
value *= self.get_field_to_current_ratio(channel) # in Gauss
value *= 1e-4 # in Tesla
else:
self.log.error('Can not read {} as field.'.format(value_as_text))
value = None
return value

def get_field_to_current_ratio(self, channel=None):
""" Return the field_to_current_ratio (G/A) for a given channel """
if channel in [1, 2]:
return float(self._field_to_current_ratio[int(channel)-1])
elif not self._dual_supply:
return float(self._field_to_current_ratio)
else:
self.log.error('Channel must be provided for dual supply.')

def get_magnet_current(self, channel=None):
""" Return the current magnet current in Tesla """
response = self._query('IMAG?', channel=channel)
return self._to_tesla(response, channel)

def set_lower_limit(self, value, channel=None):
""" Set the lower limit of the field (in Tesla) """
if not(self._limits[0] <= value <= 0):
return self.log.error('Value {} is not in the limit interval [{}, 0]'.format(value, self._limits[0]))
value_in_gauss = value * 1e4
value_in_ampere = value_in_gauss / self.get_field_to_current_ratio(channel)
self._write('REMOTE;UNITS A;LLIM {}'.format(value_in_ampere), channel=channel)

def get_lower_limit(self, channel=None):
""" Get the lower limit of the field (in Tesla) """
response = self._query('LLIM?', channel=channel)
return self._to_tesla(response, channel)

def set_upper_limit(self, value, channel=None):
""" Set the upper limit of the field (in Tesla) """
if not (0 <= value <= self._limits[1]):
return self.log.error('Value {} is not in the limit interval [0, {}]'.format(value, self._limits[1]))
value_in_gauss = value * 1e4
value_in_ampere = value_in_gauss / self.get_field_to_current_ratio(channel)
self._write('REMOTE;UNITS A;ULIM {}'.format(value_in_ampere), channel=channel)

def get_upper_limit(self, channel=None):
""" Get the upper limit of the field (in Tesla) """
response = self._query('ULIM?', channel=channel)
return self._to_tesla(response, channel)

def get_limits(self, channel=None):
""" Get the field limits as a tuple (lower_limit, higher_limit) in Tesla """
return tuple(self._limits)

def sweep(self, mode, channel=None):
""" Sweep to 'UP', 'DOWN', 'PAUSE' or 'ZERO' """
if mode in ['UP', 'DOWN', 'PAUSE', 'ZERO']:
self._write('REMOTE;SWEEP {}'.format(mode), channel=channel)

def pause(self, channel=None):
""" Pause the current sweep """
self.sweep('PAUSE', channel=channel)

def pause_all(self):
""" Pause all sweeps """
for channel in self.get_channels():
self.pause(channel)

def go_to(self, value, channel=None):
""" Set a field value (in Tesla) and directly go to it. """
if value < 0:
self.set_lower_limit(value, channel=channel)
self.sweep('DOWN', channel=channel)
elif value > 0:
self.set_upper_limit(value, channel=channel)
self.sweep('UP', channel=channel)
else:
self.sweep('ZERO', channel=channel)