https://github.com/quartiq/miqro-sim
MIQRO pulse generator simulator
https://github.com/quartiq/miqro-sim
Last synced: 5 months ago
JSON representation
MIQRO pulse generator simulator
- Host: GitHub
- URL: https://github.com/quartiq/miqro-sim
- Owner: quartiq
- Created: 2023-04-18T15:45:27.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2024-09-06T10:25:48.000Z (9 months ago)
- Last Synced: 2024-12-29T07:49:33.505Z (5 months ago)
- Language: Python
- Size: 329 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# MIQRO simulator
This is a small simulator for the MIQRO Phaser gateware and its ARTIQ interface. It's intended for experimentation and quick evaluation of the capabilities and the API. The output from the simulation can directly be used as input when integrating the differential equation of one or multiple qubits.
This work is sponsored in part by the Federal Ministry of Education and Research (BMBF) under contract 13N15524.
## Resources
The ARTIQ MIQRO coredevice driver documentation in the [ARTIQ Manual](https://m-labs.hk/artiq/manual-beta/core_drivers_reference.html#artiq.coredevice.phaser.Miqro) and here [miqro.py](miqro.py) describes the functionality and the signal flow.
## Architecture

## Data flow description
A Miqro instance represents one RF output. The DSP components are fully
contained in the Phaser gateware. The output is generated by with
the following data flow:### Oscillators
* There are `n_osc = 16` oscillators with oscillator indices `0..n_osc-1`.
* Each oscillator outputs one tone at any given time
* I/Q (quadrature, a.k.a. complex) 2x16 bit signed data
at `tau = 4 ns` sample intervals, 250 MS/s, Nyquist 125 MHz, bandwidth 200 MHz
(from `f = -100..+100 MHz`, taking into account the interpolation anti-aliasing
filters in subsequent interpolators),
* 32 bit frequency (`f`) resolution (~ 1/16 Hz),
* 16 bit unsigned amplitude (`a`) resolution
* 16 bit phase offset (`p`) resolution
* The output phase `p'` of each oscillator at time `t` (boot/reset/initialization of the
device at `t=0`) is then `p' = f*t + p (mod 1 turn)` where `f` and `p` are the (currently
active) profile frequency and phase offset.
* Note: The terms "phase coherent" and "phase tracking" are defined to refer to this
choice of oscillator output phase `p'`. Note that the phase offset `p` is not relative to
(on top of) previous phase/profiles/oscillator history.
It is "absolute" in the sense that frequency `f` and phase offset `p` fully determine
oscillator output phase `p'` at time `t`. This is unlike typical DDS behavior.
* Frequency, phase, and amplitude of each oscillator are configurable by selecting one of
`n_profile = 32` profile with indices `0..n_profile-1`. This selection is fast and can be done for
each pulse. The phase coherence defined above is guaranteed for each
profile individually.
* Note: one profile per oscillator (usually profile index 0) should be reserved
for the NOP (no operation, identity) profile, usually with zero amplitude.
* Data for each profile for each oscillator can be configured
individually. Storing profile data should be considered "expensive". "expensive" does not mean it is
impossible, just that it may take a significant amount of time and
resources to execute such that it may be impractical when used often or
during fast pulse sequences. They are intended for use in calibration and
initialization.### Summation
* The oscillator outputs are added together (wrapping addition).
* The user must ensure that the sum of oscillators outputs does not exceed the
data range. In general that means that the sum of the amplitudes must not
exceed one.### Shaper
* The summed complex output stream is then multiplied with a the complex-valued
output of a triggerable shaper.
* Triggering the shaper corresponds to passing a pulse from all oscillators to
the RF output.
* Selected profiles become active simultaneously (on the same output sample) when
triggering the shaper with the first shaper output sample.
* The shaper reads (replays) window samples from a memory of size `n_window = 1 << 10`.
* The window memory can be segmented by choosing different start indices
to support different windows.
* Each window memory segment starts with a header determining segment
length and interpolation parameters.
* The window samples are interpolated by a factor (rate change) between 1 and
`r = 1 << 12`.
* The interpolation order is constant, linear, quadratic, or cubic. This
corresponds to interpolation modes from rectangular window (1st order CIC)
or zero order hold) to Parzen window (4th order CIC or cubic spline).
* This results in support for single shot pulse lengths (envelope support) between
tau and a bit more than `r * n_window * tau = (1 << 12 + 10) tau ~ 17 ms`.
* Windows can be configured to be head-less and/or tail-less, meaning, they
do not feed zero-amplitude samples into the shaper before and after
each window respectively. This is used to implement pulses with arbitrary
length or CW output.### Overall properties
* The DAC may upconvert the signal by applying a frequency offset f1 with
phase p1.
* In the Upconverter Phaser variant, the analog quadrature upconverter
applies another frequency of f2 and phase p2.
* The resulting phase of the signal from one oscillator at the SMA output is
`(f + f1 + f2)*t + p + s(t - t0) + p1 + p2 (mod 1 turn)`
where `s(t - t0)` is the phase of the interpolated
shaper output, and `t0` is the trigger time (fiducial of the shaper).
Unsurprisingly the frequency is the derivative of the phase.
* Group delays between pulse parameter updates are matched across oscillators,
shapers, and channels.
* The minimum time to change profiles and phase offsets is ~128 ns (estimate, TBC).
This is the minimum pulse interval.
The sustained pulse rate of the RTIO PHY/Fastlink is one pulse per Fastlink frame
(may be increased, TBC).## Simulator output
Given the [example experiment](example.py):
```python
class Example(EnvExperiment):
def build(self):
self.phaser0 = miqro.Phaser()
self.miqro0 = self.phaser0.channel0.miqro@kernel
def setup(self):
# Configure example data for some profiles on some oscillators
# e.g.: profile 3 on oscillator 11 will be 3 MHz, 0.3 amplitude full scale,
# -0.3 turn (coherent) phase
for osc in [0, 4, 11]:
for profile in [1, 2, 3]:
self.miqro0.set_profile(
osc,
profile,
frequency=1 * MHz * (osc - 8),
amplitude=0.1 * profile,
phase=-0.1 * profile,
)
# Configure some window data and interpolation parameters
iq = [(1, 0), (1, 0), (0, 1), (0, 1)]
# Pulse shape will be:
# * n = len(iq) = 4 samples full scale
# * Note the window has a pi/2 phase shift for the second half.
# * r = 128 cubic (Parzen window) interpolation:
# Each window memory sample will last r tau = 512 ns and those samples
# will see cubic interpolation like this:
# Repeat each input sample r = 128 times, convolve the sequence of
# n * r samples with a rectangular window of length r = 128.
# Do that order = 3 times.
# * The output of the shaper is thus a rise to full scale,
# and then a pi/2 phase shift, then a tail to zero again, all "smooth"
# in the sense of cubic interpolation: a continuous second derivative
# of I and Q (piecewise constant third derivative).
# * Total pulse duration (shape support) is ((n + order) * r - order) * tau = 3.572 µs.
self.miqro0.set_window(start=0, iq=iq, period=128 * 4 * ns, order=3)@kernel
def pulse(self):
# Choose example profiles and phase offsets
# profiles[oscillator index] = profile index
profiles = [0] * 16
profiles[0] = 1
profiles[4] = 2
profiles[11] = 3
# Choose window start address
window = 0x000
# Trigger the pulse
# This will load frequencies and amplitudes for the oscillators,
# compute the initial oscillator phases, add the offsets,
# load the window samples, interpolate them and multiply the window with
# the sum of the oscillator outputs, all in gateware with matched latency accross
# all data paths. The DAC will interpolate further (by 4),
# shift everything by frequency f1 and then the IQ mixer will shift
# further by f2.
# The encoded pulse words can just as well be computed offline with `encode()` and then emitted with `pule_mu()` for even higher rates.
self.miqro0.pulse(window, profiles)@kernel
def run(self):
# self.phaser0.init()
# self.miqro0.reset()
self.setup()
self.pulse()
```
## Actual gateware output
For a 16-tone pulse train (limited in phase noise, noise floor, dynamic range, and distortion by measurement equipment):


## Simulator accuracy/features
The API and features described and implemented here may be slightly different from the actual gateware/ARTIQ implementation. Check back before relying on it.
Differences are:
* Integer quantization of samples not implemented
* NCO/DDS spurs, phase truncation not modelled
* IQ data overflow not implemented
* Overall latency not implemented
* Rounding not implemented
* Window head/tail features not implemented
* Lots of ARTIQ features (DMA, other devices, device db, datasets, ...)