"""
An RFSource has the properties `frequency`, `amplitude` and `phase` these can either be set or ramped linearly.
"""
from __future__ import annotations
from atomiq.components.primitives import Component, Parametrizable
from atomiq.components.electronics.voltagesource import VoltageSource
from atomiq.components.basics.calibration import Calibration
from atomiq.helper import identity_float
from artiq.experiment import kernel, portable, rpc
from artiq.language.types import TList, TFloat, TInt32, TBool
from artiq.language.core import delay
[docs]
class RFSource(Component, Parametrizable):
"""
Abstract class to represent an RF source. Typical examples of components inheriting this class
could be a DDS, a standalone AOM driver, an AWG etc.
"""
kernel_invariants = {"freq_min", "freq_max", "amp_min", "amp_max", "default_ramp_steps"}
def __init__(self,
default_frequency: TFloat = 100e6,
default_amplitude: TFloat = 0.0,
default_phase: TFloat = 0.0,
freq_limit: tuple = (0.0, float('inf')),
amp_limit: tuple = (0.0, 1.0),
blind: TBool = False,
default_ramp_steps: TInt32 = 30,
*args, **kwargs):
Component.__init__(self, *args, **kwargs)
Parametrizable.__init__(self, ["frequency", "amplitude", "phase"])
self.amplitude = default_amplitude
self.frequency = default_frequency
self.phase = default_phase
self.freq_min, self.freq_max = freq_limit
self.amp_min, self.amp_max = amp_limit
self.default_ramp_steps = default_ramp_steps
if blind:
self._prerun = Component._prerun.__get__(self)
@kernel
def _prerun(self):
self.set(self.frequency, self.amplitude, self.phase)
@kernel
def _set_frequency(self, frequency):
raise NotImplementedError
[docs]
@kernel
def set_frequency(self, frequency):
if frequency <= self.freq_max and frequency >= self.freq_min:
self._set_frequency(frequency)
else:
self.experiment.log.error("Requested frequency {0} Hz outside limits [{1}, {2}] Hz for the RFSource " +
self.identifier, [frequency, self.freq_min, self.freq_max])
[docs]
@portable
def get_frequency(self) -> TFloat:
return self.frequency
@kernel
def _set_amplitude(self, amplitude):
raise NotImplementedError
[docs]
@kernel
def set_amplitude(self, amplitude):
if amplitude <= self.amp_max and amplitude >= self.amp_min:
self._set_amplitude(amplitude)
else:
self.experiment.log.error("Requested amplitude {0} outside limits [{1}, {2}] for the RFSource " +
self.identifier, [amplitude, self.amp_min, self.amp_max])
[docs]
@portable
def get_amplitude(self) -> TFloat:
return self.amplitude
@kernel
def _set_phase(self, phase):
raise NotImplementedError
[docs]
@kernel
def set_phase(self, phase):
self._set_phase(phase)
[docs]
@portable
def get_phase(self) -> TFloat:
return self.phase
[docs]
@kernel
def set(
self,
frequency: TFloat = -1.0,
amplitude: TFloat = -1.0,
phase: TFloat = 0.0,
):
"""
Set the frequency and amplitude.
Frequency/amplitude are set to the last known value if -1 is given.
Args:
frequency: frequency [Hz]
amplitude: amplitude
"""
if frequency > 0:
self.frequency = frequency
self.set_frequency(frequency)
if amplitude > 0:
self.amplitude = amplitude
self.set_amplitude(amplitude)
if phase > 0:
self.phase = phase
self.set_phase(phase)
@kernel(flags={"fast-math"})
def _ramp(self,
duration: TFloat,
frequency_start: TFloat,
frequency_end: TFloat,
amplitude_start: TFloat,
amplitude_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 RFSource
"""
n = int(duration / ramp_timestep)
for i in range(n):
_freq = frequency_start + i * (frequency_end - frequency_start) / n
_amp = amplitude_start + i * (amplitude_end - amplitude_start) / n
self.set(frequency=_freq, amplitude=_amp)
delay(ramp_timestep)
[docs]
@kernel(flags={"fast-math"})
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 [0..1]
amplitude_end: end amplitude [0..1]
ramp_timesteps: time between steps in the ramp [s]
ramp_steps: number of steps the whole ramp should have. This takes precedence over `ramp_timesteps`
"""
frequency_start = self.frequency if frequency_start < 0 else frequency_start
frequency_end = self.frequency if frequency_end < 0 else frequency_end
amplitude_start = self.amplitude if amplitude_start < 0 else amplitude_start
amplitude_end = self.amplitude if amplitude_end < 0 else amplitude_end
if ramp_timestep > 0:
pass
elif ramp_steps > 0:
ramp_timestep = duration/ramp_steps
else:
ramp_timestep = duration/self.default_ramp_steps
self._ramp(duration, frequency_start, frequency_end, amplitude_start, amplitude_end, ramp_timestep)
self.frequency = frequency_end
self.amplitude = amplitude_end
@kernel(flags={"fast-math"})
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):
# we don't support a prepare scheme
if prepare_only:
return
num_samples = max(len(samples_amp), max(len(samples_freq), len(samples_phase)))
if num_samples == 0:
self.experiment.log.error(self.identifier + ": Could not do ARB since no samples are given")
return
t_step = duration/num_samples
for i in range(repetitions*num_samples):
_amp = transform_amp(samples_amp[i % num_samples]) if len(samples_amp) > 0 else self.amplitude
_freq = transform_freq(samples_freq[i % num_samples]) if len(samples_freq) > 0 else self.frequency
_phase = transform_phase(samples_phase[i % num_samples]) if len(samples_phase) > 0 else self.phase
self.set(_freq, _amp, _phase)
delay(t_step)
[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)
"""
# check amplitude in bounds
@kernel
def _transform_amp(x: TFloat) -> TFloat:
x_trans = transform_amp(x)
if x_trans > self.amp_max:
self.experiment.log.warning(self.identifier + ": Amplitude value {0} above upper bound {1}",
[x_trans, self.amp_max])
return self.amp_max
elif x_trans < self.amp_min:
self.experiment.log.warning(self.identifier + ": Amplitude value {0} below lower bound {1}",
[x_trans, self.amp_min])
return self.amp_min
return x_trans
# 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 x_trans
self._arb(duration, samples_amp, samples_freq, samples_phase, repetitions, prepare_only, run_prepared,
_transform_amp, _transform_freq, transform_phase)
[docs]
class RPCRFSource(RFSource):
kernel_invariants = {"rfsource"}
def __init__(self, rpc_rfsource: Component, *args, **kwargs):
RFSource.__init__(self, *args, **kwargs)
self.rfsource = rpc_rfsource
@rpc(flags={"async"})
def _set_frequency(self, frequency: TFloat):
self.rfsource.detune(frequency)
@rpc(flags={"async"})
def _set_amplitude(self, amplitude: TFloat):
self.rfsource.set_amplitude(amplitude)
@rpc(flags={"async"})
def _set_phase(self, phase: TFloat):
raise NotImplementedError
[docs]
class VoltageControlledRFSource(RFSource):
"""An RF source controlled by an analog voltage
A frequent use case for this class are AOM drivers whose frequency and amplitude can be controlled
via analog voltages.
Args:
freq_voltage_source: Voltage source that controls the frequency of the rf source
amp_voltage_source: Voltage source that controls amplitude of the rf source
freq_calibration: Calibration U = f(freq) to give the control voltage U for a desired frequency in Hz
amp_calibration: Calibration U = f(amp) to give the control voltage U for a desired amplitude
in full scale [0..1]
"""
kernel_invariants = {"freq_voltage_source", "freq_calibration", "amp_voltage_source", "amp_calibration"}
def __init__(self,
freq_voltage_source: VoltageSource = None,
freq_calibration: Calibration = None,
amp_voltage_source: VoltageSource = None,
amp_calibration: Calibration = None,
*args, **kwargs):
RFSource.__init__(self, *args, **kwargs)
def dummy(self, param: TFloat):
pass
if freq_voltage_source is not None and freq_calibration is not None:
self.freq_voltage_source = freq_voltage_source
self.freq_calibration = freq_calibration
else:
self._set_frequency = dummy.__get__(self)
self.freq_voltage_source = None
self.freq_calibration = None
if amp_voltage_source is not None and amp_calibration is not None:
self.amp_voltage_source = amp_voltage_source
self.amp_calibration = amp_calibration
else:
self._set_amplitude = dummy.__get__(self)
self.amp_voltage_source = None
self.amp_calibration = None
@kernel
def _set_frequency(self, frequency: TFloat):
self.freq_voltage_source.set_voltage(self.freq_calibration.transform(frequency))
[docs]
@kernel
def ramp_frequency(self,
duration: TFloat,
frequency_start: TFloat,
frequency_end: TFloat,
ramp_timestep: TFloat = -1.0,
ramp_steps: TInt32 = -1):
"""Ramp only the frequency of the RF source
This function can possibly ramp the RF source faster than the generic :func:`ramp` method if
the connected DAC can do fast ramps.
Args:
duration: ramp duration [s]
frequency_start: initial frequency [Hz]
frequency_end: end frequency [Hz]
ramp_timesteps: time between steps in the ramp [s]
ramp_steps: number of steps the whole ramp should have. This takes precedence over `ramp_timesteps`
"""
vstart = self.freq_calibration.transform(frequency_start)
vend = self.freq_calibration.transform(frequency_end)
self.freq_voltage_source.ramp_voltage(duration, vstart, vend, ramp_timestep, ramp_steps)
@kernel
def _set_amplitude(self, amplitude: TFloat):
self.amp_voltage_source.set_voltage(self.amp_calibration.transform(amplitude))
[docs]
@kernel
def ramp_amplitude(self,
duration: TFloat,
amplitude_start: TFloat,
amplitude_end: TFloat,
ramp_timestep: TFloat = -1.0,
ramp_steps: TInt32 = -1):
"""
Ramp only the amplitude of the RF source
This function can possibly ramp the RF source faster than the generic :func:`ramp` method if
the connected DAC can do fast ramps.
Args:
duration: ramp duration [s]
amplitude_start: initial amplitude [0..1]
amplitude_end: end amplitude [0..1]
ramp_timesteps: time between steps in the ramp [s]
ramp_steps: number of steps the whole ramp should have. This takes precedence over `ramp_timesteps`
"""
vstart = self.amp_calibration.transform(amplitude_start)
vend = self.amp_calibration.transform(amplitude_end)
self.amp_voltage_source.ramp_voltage(duration, vstart, vend, ramp_timestep, ramp_steps)
@kernel
def _set_phase(self, phase: TFloat):
# VoltageControlledRFSource does not support setting the phase
pass