Source code for atomiq.components.coil

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()