"""
An RFSource has the properties `frequency`, `amplitude` and `phase` these can either be set or ramped linearly.
"""
from __future__ import annotations
import numpy as np
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, replace_member, isnan
from artiq.experiment import kernel, portable, rpc
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.
Args:
default_frequency: Default frequency of the source.
default_amplitude: Default amplitude of the source.
default_phase: Default phase of the source.
freq_limit: Tuple of the minimum and maximum allowed frequency.
amp_limit: Tuple of the minimum and maximum allowed amplitude.
blind: Whether the source is set to its default values in each prerun phase. If `True`, the source is unchanged
during prerun, as for example necessary to generate a continuous laser lock signal.
Defaults to `False`.
default_ramp_steps: Number of constant value intervals a ramp is discretized into by default.
"""
kernel_invariants = {"freq_min", "freq_max", "amp_min", "amp_max", "default_ramp_steps"}
def __init__(
self,
default_frequency: float = 100e6,
default_amplitude: float = 0.0,
default_phase: float = 0.0,
freq_limit: tuple = (0.0, float("inf")),
amp_limit: tuple = (0.0, 1.0),
blind: bool = False,
default_ramp_steps: np.int32 = 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
self.blind = blind
if blind:
replace_member(self, "_prerun", "_prerun_blind")
[docs]
@kernel
def _prerun(self):
self.set(self.frequency, self.amplitude, self.phase)
[docs]
@kernel
def _prerun_blind(self):
pass
[docs]
@kernel
def _set_frequency(self, frequency: float):
raise NotImplementedError
[docs]
@kernel
def set_frequency(self, frequency: float):
if self._check_frequency(frequency):
self._set_frequency(frequency)
self.frequency = frequency
[docs]
@portable
def _check_frequency(self, frequency):
if frequency <= self.freq_max and frequency >= self.freq_min:
return True
else:
self.experiment.log.error(
"Requested frequency {0} outside limits [{1}, {2}] for the RFSource " + self.identifier,
[frequency, self.freq_min, self.freq_max],
)
return False
[docs]
@portable
def get_frequency(self) -> float:
return self.frequency
[docs]
@kernel
def _set_amplitude(self, amplitude: float):
raise NotImplementedError
[docs]
@kernel
def set_amplitude(self, amplitude: float):
if self._check_amplitude(amplitude):
self._set_amplitude(amplitude)
self.amplitude = amplitude
[docs]
@portable
def _check_amplitude(self, amplitude: float):
if amplitude <= self.amp_max and amplitude >= self.amp_min:
return True
else:
self.experiment.log.error(
"Requested amplitude {0} outside limits [{1}, {2}] for the RFSource " + self.identifier,
[amplitude, self.amp_min, self.amp_max],
)
return False
[docs]
@portable
def get_amplitude(self) -> float:
return self.amplitude
[docs]
@kernel
def _set_phase(self, phase: float):
raise NotImplementedError
[docs]
@kernel
def set_phase(self, phase: float):
self._set_phase(phase)
self.phase = phase
[docs]
@portable
def get_phase(self) -> float:
return self.phase
[docs]
@kernel
def set(
self,
frequency: float = float("nan"),
amplitude: float = float("nan"),
phase: float = 0.0,
):
"""Set the frequency and amplitude of the DDS channel
Frequency/amplitude are set to the last known value if ``float("nan")`` is passed (default).
Args:
frequency: Frequency in Hz (``nan`` to use previous value)
amplitude: Amplitude (``nan`` to use previous value)
phase: Phase in rad (default 0.0)
"""
if not isnan(frequency):
self.set_frequency(frequency)
if not isnan(amplitude):
self.set_amplitude(amplitude)
if phase > 0:
self.set_phase(phase)
[docs]
@kernel(flags={"fast-math"})
def _ramp(
self,
duration: float,
frequency_start: float,
frequency_end: float,
amplitude_start: float,
amplitude_end: float,
ramp_timestep: float = 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: float,
frequency_start: float = float("nan"),
frequency_end: float = float("nan"),
amplitude_start: float = float("nan"),
amplitude_end: float = float("nan"),
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = -1,
):
"""
Ramp frequency and amplitude over a given duration.
Parameters default to ``-1`` or ``nan`` to indicate no change.
If the start frequency/amplitude is set to ``nan``, 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 isnan(frequency_start) else frequency_start
frequency_end = self.frequency if isnan(frequency_end) else frequency_end
amplitude_start = self.amplitude if isnan(amplitude_start) else amplitude_start
amplitude_end = self.amplitude if isnan(amplitude_end) else amplitude_end
if not (
self._check_amplitude(amplitude_start)
and self._check_amplitude(amplitude_end)
and self._check_frequency(frequency_start)
and self._check_frequency(frequency_end)
):
return
if not isnan(ramp_timestep):
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
[docs]
@kernel(flags={"fast-math"})
def _arb(
self,
duration: float,
samples_amp: list[float],
samples_freq: list[float],
samples_phase: list[float],
repetitions: np.int32 = 1,
prepare_only: bool = False,
run_prepared: bool = 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: float,
samples_amp: list[float] = [],
samples_freq: list[float] = [],
samples_phase: list[float] = [],
repetitions: np.int32 = 1,
prepare_only: bool = False,
run_prepared: bool = 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.
Tip:
We supports a scheme to prepare the arb function before it is actually used. If that is needed, run this
function with :code:`prepapre_only = True` when the arb should be prepared and with
:code:`run_prepared = 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)
prepare_only: Only write the sequence to RAM, don't play it.
run_prepared: Play arb sequence previously prepared with :code:`prepare_only`.
transform_amp: Function to transform amplitude samples, must take a single argument of type
:type:`float` and return a single :type:`float`.
transform_freq: Function to transform frequency samples (see :code:`transform_amp`).
transform_phase: Function to transform phase samples (see :code:`transform_amp`).
"""
# check amplitude in bounds
@kernel
def _transform_amp(x: float) -> float:
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: float) -> float:
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 RFMixer(RFSource):
"""General frequency mixer with an RF, an IF and an LO port.
This component represents the signal at the RF port of a frequency mixer, i.e. the
mixer is an RFSource itself and can e.g. be used as the rfsource of an AOM. The port
frequencies always obey the mixing equation
.. math::
f_\mathrm{rf} = f_\mathrm{lo} + \mathrm{sideband} * f_\mathrm{if}
where ``sideband = -1`` corresponds to difference (down-)conversion, ``sideband = +1`` to
sum (up-)conversion and higher ``sideband`` to harmonics of the IF.
The IF and LO ports are connected to RFSource components:
* :func:`set_frequency()` adjusts the frequency of the ``frequency_control_port`` input such
that the RF output frequency matches the request, given the current frequency of
the second input. That frequency is read from the component at kernel runtime (via
:func:`RFSource.get_frequency()`), i.e. the mixer always follows the `frequency` attribute of
that component.
* :func:`set_amplitude()` is forwarded to the ``amplitude_control_port`` input.
Since this is an RFSource, ramps and arbs work through the usual machinery and are
forwarded to the corresponding input ports.
Args:
if_source: RFSource component connected to the IF port
lo_source: RFSource component connected to the LO port
sideband: nonzero integer, +1 for sum (up-), -1 for difference (down-)conversion
(default -1)
frequency_control_port: input port which is adjusted to set the RF output frequency,
"if" or "lo" (default "if")
amplitude_control_port: input port which is adjusted to set the RF output amplitude,
"if" or "lo" (default "if")
blind_source: Control whether the static source frequency is queried for every frequency update. For slow RPC
devices which do not change unexpectedly this can be set to ``False`` to avoid unnecessary RPC calls.
The frequency must then by synced manually by using :py:meth:`RFMixer.sync_static_source`.
"""
kernel_invariants = {
"if_source",
"lo_source",
"sideband",
"frequency_control_port",
"amplitude_control_port",
"_frequency_source",
"_frequency_static_source",
"_amplitude_source",
"blind_source",
}
static_frequency: float = 0.0
def __init__(
self,
if_source: RFSource,
lo_source: RFSource,
sideband: int = -1,
frequency_control_port: str = "if",
amplitude_control_port: str = "if",
blind_source: bool = False,
*args,
**kwargs,
):
if if_source is None:
raise ValueError("RFMixer: if_source is required")
if lo_source is None:
raise ValueError("RFMixer: lo_source is required")
if not isinstance(sideband, int) or sideband == 0:
raise ValueError(f"RFMixer: sideband must be a nonzero integer (got {sideband})")
if frequency_control_port not in ("if", "lo"):
raise ValueError(f"RFMixer: frequency_control_port must be 'if' or 'lo' (got '{frequency_control_port}')")
if amplitude_control_port not in ("if", "lo"):
raise ValueError(f"RFMixer: amplitude_control_port must be 'if' or 'lo' (got '{amplitude_control_port}')")
self.if_source = if_source
self.lo_source = lo_source
self.sideband = sideband
self.frequency_control_port = frequency_control_port
self.amplitude_control_port = amplitude_control_port
self._frequency_source = self.if_source if frequency_control_port == "if" else self.lo_source
self._frequency_static_source = self.lo_source if frequency_control_port == "if" else self.if_source
self._amplitude_source = self.if_source if amplitude_control_port == "if" else self.lo_source
self.blind_source = blind_source
RFSource.__init__(self, *args, **kwargs)
[docs]
@portable
def _driven_frequency(self, frequency_out: float, frequency_static: float) -> float:
if self.frequency_control_port == "if":
# f_if = (f_rf - f_lo) / sideband
return 1.0 / self.sideband * frequency_out - 1.0 / self.sideband * frequency_static
# f_lo = f_rf - sideband * f_if
return 1.0 * frequency_out - float(self.sideband) * frequency_static
[docs]
@portable
def sync_static_source(self):
"""Sync the frequency from the static frequency source.
This function is meant to be used with ``blind_source=True`` when using a non-realtime device as a static RF source
at IF or LO. If the static source is a sinara device, ``blind_source=False`` is recommended.
"""
self.static_frequency = self._frequency_static_source.get_frequency()
[docs]
@kernel
def _set_frequency(self, frequency: float):
if self.blind_source:
static_frequency = self.static_frequency
else:
static_frequency = self._frequency_static_source.get_frequency()
self._frequency_source.set_frequency(self._driven_frequency(frequency, static_frequency))
[docs]
@kernel
def _set_amplitude(self, amplitude: float):
self._amplitude_source.set_amplitude(amplitude)
[docs]
@kernel
def _set_phase(self, phase: float):
self._frequency_source.set_phase(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
[docs]
@rpc(flags={"async"})
def _set_frequency(self, frequency: float):
self.rfsource.detune(frequency)
[docs]
@rpc(flags={"async"})
def _set_amplitude(self, amplitude: float):
self.rfsource.set_amplitude(amplitude)
[docs]
@rpc(flags={"async"})
def _set_phase(self, phase: float):
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: float):
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
[docs]
@kernel
def _set_frequency(self, frequency: float):
self.freq_voltage_source.set_voltage(self.freq_calibration.transform(frequency))
[docs]
@kernel
def ramp_frequency(
self,
duration: float,
frequency_start: float,
frequency_end: float,
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = -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)
[docs]
@kernel
def _set_amplitude(self, amplitude: float):
self.amp_voltage_source.set_voltage(self.amp_calibration.transform(amplitude))
[docs]
@kernel
def ramp_amplitude(
self,
duration: float,
amplitude_start: float,
amplitude_end: float,
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = -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)
[docs]
@kernel
def _set_phase(self, phase: float):
# VoltageControlledRFSource does not support setting the phase
pass