diff --git a/documentation/changelog.md b/documentation/changelog.md
index 56ed096417..34cb411e51 100644
--- a/documentation/changelog.md
+++ b/documentation/changelog.md
@@ -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:
diff --git a/hardware/sc_magnet/cryomagnetics.py b/hardware/sc_magnet/cryomagnetics.py
new file mode 100644
index 0000000000..9f4fdaa57f
--- /dev/null
+++ b/hardware/sc_magnet/cryomagnetics.py
@@ -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 .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+
+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)