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)