"""Pre-execution processing of sweeps."""
from collections.abc import Iterable
from copy import copy
import laboneq.simple as lo
import numpy as np
from qibolab.pulses import Pulse, PulseType
from qibolab.qubits import Qubit
from qibolab.sweeper import Parameter, Sweeper
from .util import NANO_TO_SECONDS, measure_channel_name
[docs]def classify_sweepers(
sweepers: Iterable[Sweeper],
) -> tuple[list[Sweeper], list[Sweeper]]:
"""Divide sweepers into two lists: 1. sweeps that can be done in the laboneq near-time sweep loop, 2. sweeps that
can be done in real-time (i.e. on hardware)"""
nt_sweepers, rt_sweepers = [], []
for sweeper in sweepers:
if sweeper.parameter is Parameter.bias or (
sweeper.parameter is Parameter.amplitude
and sweeper.pulses[0].type is PulseType.READOUT
):
nt_sweepers.append(sweeper)
else:
rt_sweepers.append(sweeper)
return nt_sweepers, rt_sweepers
[docs]class ProcessedSweeps:
"""Data type that centralizes and allows extracting information about given
sweeps.
In laboneq, sweeps are represented with the help of SweepParameter
instances. When adding pulses to a laboneq experiment, some
properties can be set to be an instance of SweepParameter instead of
a fixed numeric value. In case of channel property sweeps, either
the relevant calibration property or the instrument node directly
can be set ot a SweepParameter instance. Parts of the laboneq
experiment that define the sweep loops refer to SweepParameter
instances as well. These should be linkable to instances that are
either set to a pulse property, a channel calibration or instrument
node. To achieve this, we use the exact same SweepParameter instance
in both places. This class takes care of creating these
SweepParameter instances and giving access to them in a consistent
way (i.e. whenever they need to be the same instance they will be
the same instance). When constructing sweep loops you may ask from
this class to provide all the SweepParameter instances related to a
given qibolab Sweeper (parallel sweeps). Later, when adding pulses
or setting channel properties, you may ask from this class to
provide all SweepParameter instances related to a given pulse or
channel, and you will get parameters that are linkable to the ones
in the sweep loop definition
"""
def __init__(self, sweepers: Iterable[Sweeper], qubits: dict[str, Qubit]):
pulse_sweeps = []
channel_sweeps = []
parallel_sweeps = []
for sweeper in sweepers:
for pulse in sweeper.pulses or []:
if sweeper.parameter in (Parameter.duration, Parameter.start):
sweep_param = lo.SweepParameter(
values=sweeper.values * NANO_TO_SECONDS
)
pulse_sweeps.append((pulse, sweeper.parameter, sweep_param))
elif sweeper.parameter is Parameter.frequency:
ptype, qubit = pulse.type, qubits[pulse.qubit]
if ptype is PulseType.READOUT:
ch = measure_channel_name(qubit)
intermediate_frequency = (
qubit.readout_frequency
- qubit.readout.local_oscillator.frequency
)
elif ptype is PulseType.DRIVE:
ch = qubit.drive.name
intermediate_frequency = (
qubit.drive_frequency
- qubit.drive.local_oscillator.frequency
)
else:
raise ValueError(
f"Cannot sweep frequency of pulse of type {ptype}, because it does not have associated frequency"
)
sweep_param = lo.SweepParameter(
values=sweeper.values + intermediate_frequency
)
channel_sweeps.append((ch, sweeper.parameter, sweep_param))
elif (
pulse.type is PulseType.READOUT
and sweeper.parameter is Parameter.amplitude
):
max_value = max(np.abs(sweeper.values))
sweep_param = lo.SweepParameter(values=sweeper.values / max_value)
# FIXME: this implicitly relies on the fact that pulse is the same python object as appears in the
# sequence that is being executed, hence the mutation is propagated. This is bad programming and
# should be fixed once things become simpler
pulse.amplitude *= max_value
channel_sweeps.append(
(
measure_channel_name(qubits[pulse.qubit]),
sweeper.parameter,
sweep_param,
)
)
else:
sweep_param = lo.SweepParameter(values=copy(sweeper.values))
pulse_sweeps.append((pulse, sweeper.parameter, sweep_param))
parallel_sweeps.append((sweeper, sweep_param))
for qubit in sweeper.qubits or []:
if sweeper.parameter is not Parameter.bias:
raise ValueError(
f"Sweeping {sweeper.parameter.name} for {qubit} is not supported"
)
sweep_param = lo.SweepParameter(
values=sweeper.values + qubit.flux.offset
)
channel_sweeps.append((qubit.flux.name, sweeper.parameter, sweep_param))
parallel_sweeps.append((sweeper, sweep_param))
for coupler in sweeper.couplers or []:
if sweeper.parameter is not Parameter.bias:
raise ValueError(
f"Sweeping {sweeper.parameter.name} for {coupler} is not supported"
)
sweep_param = lo.SweepParameter(
values=sweeper.values + coupler.flux.offset
)
channel_sweeps.append(
(coupler.flux.name, sweeper.parameter, sweep_param)
)
parallel_sweeps.append((sweeper, sweep_param))
self._pulse_sweeps = pulse_sweeps
self._channel_sweeps = channel_sweeps
self._parallel_sweeps = parallel_sweeps
[docs] def sweeps_for_pulse(
self, pulse: Pulse
) -> list[tuple[Parameter, lo.SweepParameter]]:
return [item[1:] for item in self._pulse_sweeps if item[0] == pulse]
[docs] def sweeps_for_channel(self, ch: str) -> list[tuple[Parameter, lo.SweepParameter]]:
return [item[1:] for item in self._channel_sweeps if item[0] == ch]
[docs] def sweeps_for_sweeper(self, sweeper: Sweeper) -> list[lo.SweepParameter]:
return [item[1] for item in self._parallel_sweeps if item[0] == sweeper]
[docs] def channel_sweeps_for_sweeper(
self, sweeper: Sweeper
) -> list[tuple[str, Parameter, lo.SweepParameter]]:
return [
item
for item in self._channel_sweeps
if item[2] in self.sweeps_for_sweeper(sweeper)
]
[docs] def channels_with_sweeps(self) -> set[str]:
return {ch for ch, _, _ in self._channel_sweeps}