Source code for atomiq.components.electronics.currentsource

from __future__ import annotations
from enum import IntEnum
import math

from atomiq.components.primitives import Component, Parametrizable, Switchable
from atomiq.components.electronics.voltagesource import VoltageSource
from atomiq.components.basics.calibration import Calibration

from artiq.experiment import kernel, rpc
from artiq.language.types import TFloat, TInt32, TBool
from artiq.language.core import delay


[docs] class CurrentSource(Component, Parametrizable): """Current Source This abstract class represents any device that can output a defined, controllable current. Args: min_current: The minimum current the device can output [A] max_current: The maximum current the device can output [A] default_ramp_steps: The default number of steps that this device should use if the current is ramped. This value is only used if no ``ramp_steps`` are given in the :func:`ramp_current` method. """ kernel_invariants = {"min_current", "max_current", "default_ramp_steps"} def __init__(self, min_current: TFloat = float('-inf'), max_current: TFloat = float('inf'), default_ramp_steps: TInt32 = 30, *args, **kwargs): Component.__init__(self, *args, **kwargs) Parametrizable.__init__(self, ["current"]) self.current = float('nan') self.min_current = min_current self.max_current = max_current self.default_ramp_steps = default_ramp_steps
[docs] @kernel def set_current(self, current: TFloat): """ Set the current delivered by the current source Args: current: Current in A """ if self.min_current <= current <= self.max_current: self._set_current(current) self.current = current else: self.experiment.log.warning(self.identifier + ": Trying to set current {0}A which is out of limits [{1}A, {2}A]", [current, self.min_current, self.max_current])
@kernel def _set_current(self, current: TFloat): raise NotImplementedError("Implement `_set_current()` for your current source") @kernel(flags={"fast-math"}) def _ramp_current(self, duration: TFloat, current_start: TFloat, current_end: TFloat, ramp_timestep: TFloat = 200e-6): """ This method implements a stupid ramp on an abstract level. This will most likely work but be slow. If your hardware has native support for ramping, please override this function when you inherit from currentSource """ n = int(duration / ramp_timestep) for i in range(n + 1): self._set_current(current_start + i * (current_end - current_start) / n) delay(ramp_timestep)
[docs] @kernel(flags={"fast-math"}) def ramp_current(self, duration: TFloat, current_end: TFloat, current_start: TFloat = float('nan'), ramp_timestep: TFloat = -1.0, ramp_steps: TInt32 = -1): """Ramp current over a given duration. This method advances the timeline by `duration` Args: duration: ramp duration [s] current_end: end current [A] current_start: initial current [A]. If not given, the ramp starts from the current operating current. """ # this really is `if isnan(current start)`, see IEEE 754. # `math.isnan()` and `numpy.isnan()` do not work on the core device if current_start != math.nan: # cannot use `float('nan')` on the core device current_start = self.current if self.min_current <= current_start <= self.max_current \ and self.min_current <= current_end <= self.max_current: if ramp_timestep > 0: pass elif ramp_steps > 0: ramp_timestep = duration/ramp_steps else: ramp_timestep = duration/self.default_ramp_steps self._ramp_current(duration, current_start, current_end, ramp_timestep) self.current = current_end else: self.experiment.log.warning(self.identifier + ": Trying to ramp current {0} A -> {1} A which is out of limits [{2} A, {3} A]", [current_start, current_end, self.min_current, self.max_current])
[docs] class HBridgedCurrentSource(CurrentSource, Switchable): """Combination of an H-bridge and a current source Combining an H-bridge with a unipolar current source allows to create a bipolar current source. This class bundles these two comoponents an exposes them as a bipolar current source. Args: current_source: The current source connected to the H-bridge """
[docs] class HBridgeState(IntEnum): FORWARD = 1, OFF = 0, REVERSE = -1
kernel_invariants = {"current_source"} def __init__(self, current_source: CurrentSource, *args, **kwargs): CurrentSource.__init__(self, *args, **kwargs) Switchable.__init__(self, ["switch"]) self.current_source = current_source self.state = self.HBridgeState.OFF self.last_state = self.HBridgeState.OFF
[docs] @kernel def hbridge_off(self): if self.state == self.HBridgeState.OFF: return self._hbridge_off() self.state = self.HBridgeState.OFF
[docs] @kernel def hbridge_reverse(self): if self.state == self.HBridgeState.REVERSE: return self._hbridge_reverse() self.state = self.HBridgeState.REVERSE
[docs] @kernel def hbridge_forward(self): if self.state == self.HBridgeState.FORWARD: return self._hbridge_forward() self.state = self.HBridgeState.FORWARD
[docs] @kernel def hbridge_toggle(self): if self.state == self.HBridgeState.FORWARD: self.hbridge_reverse() elif self.state == self.HBridgeState.REVERSE: self.hbridge_forward() else: self.on()
@kernel def _hbridge_off(self): raise NotImplementedError("implement _hbridge_off() in subclass") @kernel def _hbridge_forward(self): raise NotImplementedError("implement _hbridge_forward() in subclass") @kernel def _hbridge_reverse(self): raise NotImplementedError("implement _hbridge_reverse() in subclass") @kernel def _set_current(self, current: TFloat): if current < 0: self.hbridge_reverse() self.current_source.set_current(-1.*current) elif current == 0.0: self.hbridge_off() self.current_source.set_current(current) else: self.hbridge_forward() self.current_source.set_current(current) @kernel(flags={"fast-math"}) def _ramp_current(self, duration: TFloat, current_start: TFloat, current_end: TFloat, ramp_timestep: TFloat): if current_start < 0.0 or (current_start == 0.0 and current_end < 0): self.hbridge_reverse() else: self.hbridge_forward() # ramps with zero slope if current_end == current_start: self.current_source.set_current(abs(current_start)) delay(duration) return # calculate time to flip polarity m = (current_end - current_start) / duration b = current_start t_0 = -b / m current_end = abs(current_end) current_start = abs(current_start) if t_0 <= 0 or t_0 >= duration: # unipolar ramp self.current_source.ramp_current(duration=duration, current_start=current_start, current_end=current_end, ramp_timestep=ramp_timestep) else: # bipolar ramp self.current_source.ramp_current(duration=t_0, current_start=current_start, current_end=0.0, ramp_timestep=ramp_timestep) self.hbridge_toggle() self.current_source.ramp_current(duration=duration - t_0, current_start=0.0, current_end=current_end, ramp_timestep=ramp_timestep)
[docs] @kernel def off(self): self.last_state = self.state self.hbridge_off()
[docs] @kernel def on(self): if self.last_state == self.HBridgeState.FORWARD: self.hbridge_forward() elif self.last_state == self.HBridgeState.REVERSE: self.hbridge_reverse() else: self.experiment.log.error("Cannot turn on H-bridge without knowledge of prior state (FORWARD or REVERSE)!")
[docs] class TTLHardwareLogicHBridgedCurrentSource(HBridgedCurrentSource): """H-bridged current source with control logic implemented in hardware Some external hardware (logic gates) take care to set all MOSFETs of the H-bridge based on the desired direction as indicated by `switch_direction`. Via `switch_on` the entire bridge can be enabled and disabled. +---------------+----------------------+---------------------+ | ``switch_on`` | ``switch_direction`` | current flow | +===============+======================+=====================+ | off | on | off | +---------------+----------------------+---------------------+ | off | off | off | +---------------+----------------------+---------------------+ | on | on | forward | +---------------+----------------------+---------------------+ | on | off | reverse | +---------------+----------------------+---------------------+ Args: switch_on: When ON, H-bridge is eith forward or reverse; when OFF, load is disconnected from the PSU switch_direction: select forward or reverse direction of current flow invert_direction: flip forward/reverse """ kernel_invariants = {"switch_direction", "switch_on", "invert_direction"} def __init__(self, switch_direction: Switchable, switch_on: Switchable, invert_direction: TBool = False, *args, **kwargs): HBridgedCurrentSource.__init__(self, *args, **kwargs) self.switch_direction = switch_direction self.switch_on = switch_on self.invert_direction = invert_direction @kernel def _hbridge_off(self): self.switch_on.off() @kernel def _hbridge_forward(self): if self.invert_direction: self.switch_direction.off() else: self.switch_direction.on() self.switch_on.on() @kernel def _hbridge_reverse(self): if self.invert_direction: self.switch_direction.on() else: self.switch_direction.off() self.switch_on.on()
[docs] class TTLSoftwareLogicHBridgedCurrentSource(HBridgedCurrentSource): """H-bridged current source with control logic implemented in software Each pair of MOSFETs is directly controlled by one switch. So when both TTLs are off, the bridge is off, but also when both are on the PSU is shorted. +--------------------+--------------------+--------------------+ | ``switch_forward`` | ``switch_reverse`` | current flow | +====================+====================+====================+ | off | off | off | +--------------------+--------------------+--------------------+ | off | on | reverse | +--------------------+--------------------+--------------------+ | on | off | forward | +--------------------+--------------------+--------------------+ | on | on | INVALID | +--------------------+--------------------+--------------------+ Args: switch_forward: TTL to enable forward pair of MOSFETs switch_reverse: TTL to enable reverse pair of MOSFETs """ kernel_invariants = {"switch_forward", "switch_reverse"} def __init__(self, switch_forward: Switchable, switch_reverse: Switchable, *args, **kwargs): HBridgedCurrentSource.__init__(self, *args, **kwargs) self.switch_forward = switch_forward self.switch_reverse = switch_reverse @kernel def _hbridge_off(self): self.switch_forward.off() self.switch_reverse.off() @kernel def _hbridge_forward(self): self.switch_forward.on() self.switch_reverse.off() @kernel def _hbridge_reverse(self): self.switch_forward.off() self.switch_reverse.on()
[docs] class RPCCurrentSource(CurrentSource): """A current source controlled via RPC calls Args: rpc_currentsource: The ARTIQ rpc object representing the current source. This object needs to provide a function named `set_current(current_in_A)` to set the current. """ kernel_invariants = {"currentsource"} def __init__(self, rpc_currentsource: Component, *args, **kwargs): CurrentSource.__init__(self, *args, **kwargs) self.currentsource = rpc_currentsource @rpc(flags={"async"}) def _set_current(self, current: TFloat): self.currentsource.set_current(current)
[docs] class RPCCurrentSourceChannel(CurrentSource): """One channel of a multi-channel currentsource controlled via RPC Args: rpc_currentsource: The ARTIQ rpc object representing the multi channel current source. This object needs to provide a function named `set_current(current_in_A, channel)` to set the current. channel: channel of the multi-channel current source to operate on """ kernel_invariants = {"currentsource", "channel"} def __init__(self, rpc_currentsource: Component, channel: TInt32, *args, **kwargs): CurrentSource.__init__(self, *args, **kwargs) self.currentsource = rpc_currentsource self.channel = channel @rpc(flags={"async"}) def _set_current(self, current: TFloat): self.currentsource.set_current(current, self.channel)
[docs] class VoltageControlledCurrentSource(CurrentSource): """A current source controlled by an analog voltage A typical usecase for this class are voltage-controlled power supplies that drive the current through a coil. Args: voltage_source: Voltage source that controls the current source calibration: Calibration U = f(I) to give the control voltage U for a desired current I """ kernel_invariants = {"voltage_source", "calibration"} def __init__(self, voltage_source: VoltageSource, calibration: Calibration, *args, **kwargs): CurrentSource.__init__(self, *args, **kwargs) self.voltage_source = voltage_source self.calibration = calibration @kernel def _set_current(self, current: TFloat): self.voltage_source.set_voltage(self.calibration.transform(current)) @kernel def _ramp_current(self, duration: TFloat, current_start: TFloat, current_end: TFloat, ramp_timestep: TFloat): self.voltage_source.ramp_voltage(duration, self.calibration.transform(current_start), self.calibration.transform(current_end), ramp_timestep)