from __future__ import annotations
from atomiq.components.primitives import Component, Parametrizable, Switchable
from atomiq.components.electronics.rfsource import RFSource
from atomiq.components.basics.calibration import Calibration
from atomiq.helper import identity_float, replace_member
from artiq.experiment import kernel, portable, delay
from artiq.language.types import TBool, TFloat, TInt32, TList
from artiq.language.units import s
[docs]
class Shutter(Component, Switchable):
"""
Component to switch light on or off depending on a logical signal. This could be a mechanical shutter or a binary
only amplitude modulator (e.g. AOM, EOM, Pockels cell etc.). It requires a class:Switchable switch that operates
the shutter
Args:
switch: Switch that operates the shutter, e.g. TTL
invert: invert the logic of on and off
opening_time: Time in s it takes from the arrival of the TTL until the shutter is fully opened (default 0)
closing_time: Time in s it takes from the arrival of the TTL until the shutter is completely closed (default 0).
Note that if closing_time is > 0 the shutter is already closing before the time at which the off()
method is called
"""
kernel_invariants = {"switch", "invert"}
def __init__(self, switch: Switchable, invert: TBool = False, opening_time: TFloat = 0, closing_time: TFloat = 0,
*args, **kwargs):
Component.__init__(self, *args, **kwargs)
Switchable.__init__(self, ["blank"])
self.switch = switch
self.invert = invert
self.opening_time = opening_time
self.closing_time = closing_time
[docs]
@kernel
def on(self):
"""
Opens the shutter. The time cursor is moved back in time to accommodate the `opening_time` of the shutter.
After executing the `on` (or `off` if inverted) method of the defined switch the time cursor is then forwarded
by `opening_time` again. The time cursor advancement is therefore given by the `switch.on()` or
`switch.off()` method.
"""
delay(-self.opening_time*s)
if not self.invert:
self.switch.on()
else:
self.switch.off()
delay(self.opening_time*s)
[docs]
@kernel
def off(self):
"""
Closes the shutter. The time cursor is moved back in time to accommodate the `closing_time` of the shutter.
After executing the `off` (or `on` if inverted) method of the defined switch the time cursor is then forwarded
by `closing_time` again. The time cursor advancement is therefore given by the `switch.on()` or
`switch.off()` method.
"""
delay(-self.closing_time*s)
if not self.invert:
self.switch.off()
else:
self.switch.on()
delay(self.closing_time*s)
[docs]
class LightModulator(Component, Parametrizable):
"""An abstract light modulator for frequency, amplitude, phase, polarisation
This class serves as a base class for all kinds of electro-optic devices that
can change the properties of light.
"""
def __init__(self, *args, **kwargs):
Component.__init__(self, *args, **kwargs)
Parametrizable.__init__(self, ["frequency", "amplitude", "phase"])
[docs]
@kernel
def set_frequency(self, frequency: TFloat):
"""Set the frequency by which the light is shifted.
Args:
frequency: Frequency in Hz by which the light is shifted.
"""
raise NotImplementedError("Implement `set_frequency` for your light modulator")
[docs]
@kernel
def get_frequency(self) -> TFloat:
raise NotImplementedError("Implement `get_frequency` for your light modulator")
[docs]
@kernel
def set_amplitude(self, amplitude: TFloat):
"""Set the amplitude of the light after the modulator
Args:
amplitude: Relative amplitude [0 .. 1] of the light after the modulator
"""
raise NotImplementedError("Implement `set_amplitude` for your light modulator")
[docs]
@kernel
def set_phase(self, phase: TFloat):
"""Set the phase shift of the light imposed by the modulator
Args:
phase: Phase shift in radians
"""
raise NotImplementedError("Implement `set_phase` for your light modulator")
[docs]
@kernel
def set_polarisation(self, angle: TFloat):
"""Set the polarization rotation imposed by the modulator
Args:
angle: Rotation angle in radians
"""
raise NotImplementedError("Implement `set_polarisation` for your light modulator")
[docs]
class RFLightModulator(LightModulator):
"""A light modulator driven by an RF source
This class serves as a base class for devices like AOM, EOM, etc.
Args:
rfsource: The rfsource that drives the modulator
freq_limit: Tuple (freq_min, freq_max) giving the minimum/maximum RF frequency that the
modulator can handle in Hz.
amp_limit: Tuple (amp_min, amp_max) giving the minimum/maximum RF attenuation that the
modulator can do in range [0..1].
"""
kernel_invariants = {"rfsource", "freq_min", "freq_max", "amp_min", "amp_max"}
def __init__(self, rfsource: RFSource, freq_limit: tuple, amp_limit: tuple = (0.0, 1.0), *args, **kwargs):
LightModulator.__init__(self, *args, **kwargs)
self.rfsource = rfsource
if isinstance(rfsource, Switchable):
self.switch = self.rfsource
else:
self.on = lambda: self.experiment.log.warning("Modulator " + self.identifier +
" has no switch that can be used to switch on")
self.off = lambda: self.experiment.log.warning("Modulator " + self.identifier +
"has no switch that can be used to switch off")
self.freq_min, self.freq_max = freq_limit
self.amp_min, self.amp_max = amp_limit
[docs]
@kernel
def ramp(self,
duration: TFloat,
frequency_start: TFloat = -1.0,
frequency_end: TFloat = -1.0,
amplitude_start: TFloat = -1.0,
amplitude_end: TFloat = -1.0,
ramp_timestep: TFloat = -1.0,
ramp_steps: TInt32 = -1):
"""Ramp frequency and amplitude over a given duration.
Parameters default to -1 to indicate no change.
If the start frequency/amplitude is set to -1, the ramp starts from the last frequency/amplitude which was set.
This method advances the timeline by ´duration´
Args:
duration: ramp duration [s]
frequency_start: initial frequency [Hz]
frequency_end: end frequency [Hz]
amplitude_start: initial amplitude [Hz]
amplitude_end: end amplitude [Hz]
"""
if frequency_start >= 0 and (frequency_start < self.freq_min or frequency_start > self.freq_max):
self.experiment.log.error("Requested start frequency {0} Hz in ramp outside limits [{1}, {2}] Hz for the \
modulator " + self.identifier, [frequency_start, self.freq_min, self.freq_max])
return
if frequency_end >= 0 and (frequency_end < self.freq_min or frequency_end > self.freq_max):
self.experiment.log.error("Requested end frequency {0} Hz in ramp outside limits [{1}, {2}] Hz for the \
modulator " + self.identifier, [frequency_end, self.freq_min, self.freq_max])
return
self.rfsource.ramp(duration, frequency_start, frequency_end,
amplitude_start, amplitude_end, ramp_timestep, ramp_steps)
[docs]
@kernel
def arb(self,
duration: TFloat,
samples_amp: TList(TFloat) = [],
samples_freq: TList(TFloat) = [],
samples_phase: TList(TFloat) = [],
repetitions: TInt32 = 1,
prepare_only: TBool = False,
run_prepared: TBool = False,
transform_amp=identity_float,
transform_freq=identity_float,
transform_phase=identity_float
):
"""Play Arbitrary Samples from a List
This method allows to set the output amplitude, frequency an phase according to the values specified in
respective lists. The whole sequence is played in the specified duration. The pattern store in the sample
list can also be repeated.
We supports a scheme to prepare the arb function before it is actually used. If that is needed, run this
function with `prepapre_only = True` when the arb should be prepared and with `run_only = True` when the
prepared arb should be played. In both calls the other parameters have to be passed.
Args:
samples_amp: List of amplitude samples. If this list is empty (default), the amplitude is not modified.
samples_freq: List of frequency samples. If this list is empty (default), the frequency is not modified.
samples_phase: List of phase samples. If this list is empty (default), the phase is not modified.
duration: The time in which the whole sequence of samples should be played back [s].
repetitions: Number of times the sequence of all samples should be played. (default 1)
"""
self._arb(duration, samples_amp, samples_freq, samples_phase, repetitions, prepare_only, run_prepared,
transform_amp, transform_freq, transform_phase)
@kernel
def _arb(self,
duration: TFloat,
samples_amp: TList(TFloat) = [],
samples_freq: TList(TFloat) = [],
samples_phase: TList(TFloat) = [],
repetitions: TInt32 = 1,
prepare_only: TBool = False,
run_prepared: TBool = False,
transform_amp=identity_float,
transform_freq=identity_float,
transform_phase=identity_float
):
# check frequency in bounds
@kernel
def _transform_freq(x: TFloat) -> TFloat:
x_trans = transform_freq(x)
if x_trans > self.freq_max:
self.experiment.log.warning(self.identifier + ": Frequency value {0} above upper bound {1}",
[x_trans, self.freq_max])
return self.freq_max
elif x_trans < self.freq_min:
self.experiment.log.warning(self.identifier + ": Frequency value {0} below lower bound {1}",
[x_trans, self.freq_min])
return self.freq_min
return transform_freq(x)
self.rfsource.arb(duration, samples_amp, samples_freq, samples_phase, repetitions, prepare_only, run_prepared,
transform_amp, _transform_freq, transform_phase)
[docs]
@portable
def get_frequency_min(self) -> TFloat:
return self.freq_min
[docs]
@portable
def get_frequency_max(self) -> TFloat:
return self.freq_max
[docs]
@portable
def get_frequency(self) -> TFloat:
return self.rfsource.get_frequency()
@kernel
def _check_and_set_frequency(self, frequency: TFloat):
if frequency >= self.freq_min and frequency <= self.freq_max:
self.rfsource.set_frequency(frequency)
else:
self.experiment.log.error("Requested frequency {0} Hz outside limits [{1}, {2}] Hz for the modulator " +
self.identifier, [frequency, self.freq_min, self.freq_max])
[docs]
@kernel
def set_frequency(self, frequency: TFloat):
self._check_and_set_frequency(frequency)
[docs]
@portable
def get_amplitude_min(self) -> TFloat:
return self.amp_min
[docs]
@portable
def get_amplitude_max(self) -> TFloat:
return self.amp_max
[docs]
@portable
def get_amplitude(self) -> TFloat:
return self.rfsource.get_amplitude()
@kernel
def _check_and_set_amplitude(self, amplitude: TFloat):
if amplitude >= self.amp_min and amplitude <= self.amp_max:
self.rfsource.set_amplitude(amplitude)
else:
self.experiment.log.error("Requested amplitude {0} outside limits [{1}, {2}] for the modulator " +
self.identifier, [amplitude, self.amp_min, self.amp_max])
[docs]
@kernel
def set_amplitude(self, amplitude: TFloat):
self._check_and_set_amplitude(amplitude)
[docs]
@portable
def get_phase(self) -> TFloat:
return self.rfsource.get_phase()
[docs]
@kernel
def set_phase(self, value: TFloat):
self.rfsource.set_phase(value)
[docs]
@kernel
def on(self):
self.switch.on()
[docs]
@kernel
def off(self):
self.switch.off()
[docs]
class AOM(RFLightModulator, Switchable):
"""An acousto-optical modulator to alter amplitude, frequency and phase of the light.
A component to represent an AOM to attenuate, switch and frequency-shift light. It is controlled by an
:class:`~atomiq.components.electronics.rfsource.RFSource` and a :class:`~atomiq.components.primitives.Switchable`
to rapidly switch the light on and off. As such, the AOM works also as a (non-perfect) shutter.
Args:
rfsource: The rf source that drives the AOM
center_freq: RF center frequency of the AOM in Hz.
freq_limit: Tuple (freq_min, freq_max) giving the minimum/maximum RF frequency that the AOM can handle in Hz.
Either freq_limit xor bandwidth must be given
bandwidth: RF bandwidth of the AOM around the center frequency in Hz.
Either bandwidth xor freq_limit must be given.
switch: An optional switch to rapidly switch on and off the AOM. If none is given
the rfsource is used to switch.
switching_delay: the switching delay of the AOM, i.e. the time it takes from the arrival of the TTL to having
full optical power. (default 0)
passes: How often does the beam pass the AOM? Singlepass -> 1, Doublepass -> 2 (default 1)
order: The diffraction order the AOM is aligned to. -2, -1, 1, 2 ... (default 1)
am_calibration: Calibration of RF power vs output power. Typically an inverse sigmoid. (default none)
"""
kernel_invariants = {"center_freq", "switching_delay", "passes", "order"}
def __init__(self,
center_freq: TFloat,
freq_limit: tuple = None,
bandwidth: TFloat = None,
switch: Switchable = None,
switching_delay: TFloat = 0,
passes: TInt32 = 1,
order: TInt32 = 1,
am_calibration: Calibration = None,
*args, **kwargs):
if freq_limit is None and bandwidth is None:
raise ValueError(f"For component {kwargs['identifier']} either `bandwidth` or `freq_limit` must be set")
if bandwidth is not None:
freq_limit = (center_freq - bandwidth/2., center_freq + bandwidth/2.)
kwargs["freq_limit"] = freq_limit
RFLightModulator.__init__(self, *args, **kwargs)
Switchable.__init__(self, ["blank"])
# Add the am calibration if one is given
if am_calibration is not None:
self.am_calibration = am_calibration
replace_member(self, "_amplitude_transform", "_amplitude_transform_calibration")
else:
replace_member(self, "_amplitude_transform", "_amplitude_transform_identity")
# Add to Parametrizable
self.channels.append("detuning")
self.center_freq = center_freq
self.switching_delay = switching_delay
self.passes = passes
self.order = order
@kernel
def _prerun(self):
self.detune(0.0)
[docs]
@portable
def get_frequency_min(self) -> TFloat:
return self.passes*self.order*self.freq_min
[docs]
@portable
def get_frequency_max(self) -> TFloat:
return self.passes*self.order*self.freq_max
[docs]
@portable
def get_frequency(self) -> TFloat:
return self.order*self.passes*self.rfsource.get_frequency()
[docs]
@kernel
def set_frequency(self, frequency: TFloat):
"""
Set the frequency shift of the light coming out of the AOM
"""
self._check_and_set_frequency(frequency/(self.passes*self.order))
[docs]
@kernel
def set_detuning(self, detuning: TFloat):
"""
Set the frequency shift of the light coming out of the AOM relative to the center frequency
Args:
detuning: Detuning from the AOM center frequency in Hz
"""
self.set_frequency(self.center_freq*self.passes*self.order + detuning)
[docs]
@kernel
def detune(self, detuning: TFloat):
""" Alias for set_detuning() """
self.set_detuning(detuning)
@kernel
def _amplitude_transform_calibration(self, amplitude: TFloat) -> TFloat:
return self.am_calibration.transform(amplitude)
@kernel
def _amplitude_transform_identity(self, amplitude: TFloat) -> TFloat:
return amplitude
[docs]
@kernel
def set_amplitude(self, amplitude: TFloat):
self._check_and_set_amplitude(self._amplitude_transform(amplitude))
[docs]
@kernel
def arb(self,
duration: TFloat,
samples_amp: TList(TFloat) = [],
samples_freq: TList(TFloat) = [],
samples_det: TList(TFloat) = [],
samples_phase: TList(TFloat) = [],
repetitions: TInt32 = 1,
prepare_only: TBool = False,
run_prepared: TBool = False,
transform_amp=identity_float,
transform_freq=identity_float,
transform_phase=identity_float
):
"""Play Arbitrary Samples from a List
This method allows to set the output amplitude, frequency an phase according to the values specified in
respective lists. The whole sequence is played in the specified duration. The pattern store in the sample
list can also be repeated.
We supports a scheme to prepare the arb function before it is actually used. If that is needed, run this
function with `prepapre_only = True` when the arb should be prepared and with `run_only = True` when the
prepared arb should be played. In both calls the other parameters have to be passed.
Args:
samples_amp: List of amplitude samples. If this list is empty (default), the amplitude is not modified.
samples_freq: List of frequency samples. If this list is empty (default), the frequency is not modified.
samples_det: List of frequency samples relative to the center frequency. If this list is empty (default),
the frequency is not modified. This overwrites `samples_frequency`
samples_phase: List of phase samples. If this list is empty (default), the phase is not modified.
duration: The time in which the whole sequence of samples should be played back [s].
repetitions: Number of times the sequence of all samples should be played. (default 1)
"""
_transform_freq = identity_float
_transform_det = identity_float
def _transform_amp(x):
return self._amplitude_transform(transform_amp(x))
if len(samples_det) > 0:
def _transform_det(x):
return transform_freq(x)/(self.passes*self.order) + self.center_freq
self._arb(duration, samples_amp, samples_det, samples_phase, repetitions, prepare_only, run_prepared,
_transform_amp, _transform_det, transform_phase)
else:
def _transform_freq(x):
return transform_freq(x)/(self.passes*self.order)
self._arb(duration, samples_amp, samples_freq, samples_phase, repetitions, prepare_only, run_prepared,
_transform_amp, _transform_freq, transform_phase)
[docs]
@kernel
def on(self):
"""
Turns on the AOM. The time cursor is moved back in time to accommodate the `switching_delay` of the AOM. After
executing the `on` method of the defined switch the time cursor is then forwarded by `switching_delay` again.
The time cursor advancement is therefore given by the `switch.on()` method.
"""
delay(-self.switching_delay*s)
self.switch.on()
delay(self.switching_delay*s)
[docs]
@kernel
def off(self):
"""
Turns off the AOM. The time cursor is moved back in time to accommodate the `switching_delay` of the AOM. After
executing the `off` method of the defined switch the time cursor is then forwarded by `switching_delay` again.
The time cursor advancement is therefore given by the `switch.off()` method.
"""
delay(-self.switching_delay*s)
self.switch.off()
delay(self.switching_delay*s)