from __future__ import annotations
import math
from atomiq.components.primitives import Component, Parametrizable, Switchable
from atomiq.components.electronics.currentsource import CurrentSource
from atomiq.components.basics.calibration import Calibration
from atomiq.helper import replace_member
from artiq.experiment import kernel, parallel
from artiq.language.types import TInt32, TFloat, TBool
[docs]
class Coil(Component, Parametrizable, Switchable):
"""
A single coil driven by a current source.
It is characterized by a calibration that returns the B field for a given current through the coil.
Args:
current_source: The current source that drives the coil
calibration: The conversion between current in the coil and magnetic field at the point of interest.
switch: An optional switch to switch on and off the current in the coil. If none is given and the current
source is switchable, the current source is used for switching. If no switch is given and the current
source is not switchable, an error is raised.
"""
kernel_invariants = {"current_source", "calibration", "switch"}
def __init__(self, current_source: CurrentSource, calibration: Calibration, switch: Switchable = None,
*args, **kwargs):
Component.__init__(self, *args, **kwargs)
Parametrizable.__init__(self, ["field"])
Switchable.__init__(self, ["powered"])
self.current_source = current_source
self.calibration = calibration
if switch is not None:
self.switch = switch
elif isinstance(current_source, Switchable):
self.switch = current_source
else:
raise AttributeError(
"Cannot switch coil: Neither current_source is switchable nor a switch is given.")
[docs]
@kernel
def set_field(self, field):
self.current_source.set_current(self.calibration.transform(field))
[docs]
@kernel
def on(self):
self.switch.on()
[docs]
@kernel
def off(self):
self.switch.off()
[docs]
class CoilPair(Component, Parametrizable, Switchable):
"""
A pair of identical coils driven by a common or two individual current sources.
It is assumed that the pair of coils is placed symmetrically around the region of interest.
It is characterized by a calibration (field_calibration) for the returns the B field for a given current through
the coil and a calibration of the field gradient
Args:
current_source_coil1: The current source for the first coil.
current_source_coil2: The current source for the second coil. If the second coil is tied to the same current
source as the first coil, specify the same current source here. Use the
`helmholtz_config` parameter to specifiy the b-field orientation of the second coil.
field_calibration: Calibration that gives the B field as a function of the current at the position of interest
if the coilpair is in Helmholtz configuration
gradient_calibration: Calibration that gives the B field gradient as a function of the current difference
between the coils at the position of interest if the coilpair is in anti-Helmholtz
configuration
switch: Switch that switches the coil pair on and off. If none is given the current source is used to switch
the coils (if it supports switching). (default None)
helmholtz_config: if set to 1, same polarity of current in both coils creates a Helmholtz field. If set to -1
same polarity of current in both coils creates an anti-Helmholtz field. (default 1)
default_field: Field to initialize the coil pair with (defaul 0)
default_gradient: Gradient to initialize the coil pair with (default 0)
"""
kernel_invariants = {"current_source_coil1", "current_source_coil2", "single_source", "helmholtz_config",
"calibration_field", "calibration_gradient", "switch"}
def __init__(self,
current_source_coil1: CurrentSource,
current_source_coil2: CurrentSource,
calibration_field: Calibration,
calibration_gradient: Calibration,
switch: Switchable = None,
helmholtz_config: TInt32 = 1,
default_field: TFloat = 0.0,
default_gradient: TFloat = 0.0,
*args, **kwargs):
Component.__init__(self, *args, **kwargs)
Parametrizable.__init__(self, ["field", "gradient"])
Switchable.__init__(self, ["powered"])
self.current_source_coil1 = current_source_coil1
self.current_source_coil2 = current_source_coil2
self.single_source = (current_source_coil1 == current_source_coil2)
self.helmholtz_config = helmholtz_config
self.calibration_field = calibration_field
self.calibration_gradient = calibration_gradient
self.field = default_field
self.gradient = default_gradient
if switch is not None:
self.switch = switch
elif isinstance(current_source_coil1, Switchable) and isinstance(current_source_coil2, Switchable):
replace_member(self, "on", "_switch_on_via_currentsource")
replace_member(self, "off", "_switch_off_via_currentsource")
self.switch = None
else:
raise AttributeError("Cannot switch coil: Neither current_source is switchable nor a switch is given.")
@kernel
def _prerun(self):
self._set_currents()
@kernel
def _set_currents(self):
self.current_source_coil1.set_current(self.calibration_field.transform(self.field) +
self.calibration_gradient.transform(self.gradient))
if not self.single_source:
self.current_source_coil2.set_current(self.helmholtz_config *
(self.calibration_field.transform(self.field) -
self.calibration_gradient.transform(self.gradient)))
[docs]
@kernel
def set_field(self, field: TFloat, update: TBool = True):
"""Set the field of the coil pair at the position if interest
Args:
field: Field to be set in the units of the calibration `field_calibration`
update: immediately apply the change to the hardware
"""
self.field = field
if update:
self._set_currents()
[docs]
@kernel
def set_gradient(self, gradient: TFloat, update: TBool = True):
"""Set the field gradient of the coil pair at the position of interest
Args:
gradient: Field gradient to be set in the units of the calibration `field_calibration`
update: immediately apply the change to the hardware
"""
self.gradient = gradient
if update:
self._set_currents()
[docs]
@kernel
def ramp_field_and_gradient(self, duration: TFloat,
field_end: TFloat, gradient_end: TFloat,
field_start: TFloat = float('nan'), gradient_start: TFloat = float('nan'),
ramp_timestep: TFloat = 1e-3):
"""ramp the field and gradient thereof created by a pair of coils
Note that this only works as expected with two independent currentsources.
This method advances the timeline by `duration`.
Args:
duration: duration of the ramp
field_end: final field at the end of the ramp
gradient_end: final gradient at the end of the ramp
field_start: starting field of the ramp. Current field is used if not given.
gradient_start: starting gradient of the ramp. Current gradient is used if not given.
ramp_timestep: sampling interval of the ramp
"""
# this really is `if isnan(...):`, see IEEE 754.
# `math.isnan()` and `numpy.isnan()` do not work on the core device
if field_start != math.nan: # cannot use `float('nan')` on the core device
field_start = self.field
if gradient_start != math.nan:
gradient_start = self.gradient
if self.single_source and gradient_end != gradient_start and field_start != field_end:
self.experiment.log.error(self.identifier + ": cannot ramp field and gradient with both coils \
controlled by the same current source")
# precompute transforms once to reduce computational overhead
current_field_start = self.calibration_field.transform(field_start)
current_field_end = self.calibration_field.transform(field_end)
current_gradient_start = self.calibration_gradient.transform(gradient_start)
current_gradient_end = self.calibration_gradient.transform(gradient_end)
current_1_start = current_field_start + current_gradient_start
current_1_end = current_field_end + current_gradient_end
if self.single_source:
self.current_source_coil1.ramp_current(duration, current_start=current_1_start, current_end=current_1_end,
ramp_timestep=ramp_timestep)
else:
current_2_start = self.helmholtz_config * (current_field_start - current_gradient_start)
current_2_end = self.helmholtz_config * (current_field_end - current_gradient_end)
with parallel:
self.current_source_coil1.ramp_current(duration, current_start=current_1_start,
current_end=current_1_end, ramp_timestep=ramp_timestep)
self.current_source_coil2.ramp_current(duration, current_start=current_2_start,
current_end=current_2_end, ramp_timestep=ramp_timestep)
self.gradient = gradient_end
self.field = field_end
[docs]
@kernel
def ramp_field(self, duration: TFloat, field_end: TFloat, field_start: TFloat = float('nan'),
ramp_timestep: TFloat = 1e-3):
"""ramp the field created by pair of coils
This method advances the timeline by `duration`.
Args:
duration: duration of the ramp
field_end: final field at the end of the ramp
field_start: starting field of the ramp. Current field is used if not given.
ramp_timestep: sampling interval of the ramp
"""
self.ramp_field_and_gradient(duration=duration,
field_start=field_start, field_end=field_end,
gradient_start=self.gradient, gradient_end=self.gradient,
ramp_timestep=ramp_timestep)
[docs]
@kernel
def ramp_gradient(self, duration: TFloat, gradient_end: TFloat, gradient_start: TFloat = float('nan'),
ramp_timestep: TFloat = 1e-3):
"""ramp the gradient created by pair of coils
This method advances the timeline by `duration`.
Args:
duration: duration of the ramp
gradient_end: final gradient at the end of the ramp
gradient_start: starting gradient of the ramp. Current gradient is used if not given.
ramp_timestep: sampling interval of the ramp
"""
self.ramp_field_and_gradient(duration=duration,
field_start=self.field, field_end=self.field,
gradient_start=gradient_start, gradient_end=gradient_end,
ramp_timestep=ramp_timestep)
[docs]
@kernel
def on(self):
self.switch.on()
[docs]
@kernel
def off(self):
self.switch.off()
@kernel
def _switch_on_via_currentsource(self):
self.current_source_coil1.on()
if not self.single_source:
self.current_source_coil2.on()
@kernel
def _switch_off_via_currentsource(self):
self.current_source_coil1.off()
if not self.single_source:
self.current_source_coil2.off()