Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Sweeper pydantic Model and introduce range #1014

Merged
merged 15 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/getting-started/experiment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her
f0 = platform.config(str(qubit.probe.name)).frequency # center frequency
sweeper = Sweeper(
parameter=Parameter.frequency,
values=f0 + np.arange(-2e8, +2e8, 1e6),
range=(f0 - 2e8, f0 + 2e8, 1e6),
channels=[qubit.probe.name],
)

Expand Down
18 changes: 7 additions & 11 deletions doc/source/main-documentation/qibolab.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ In the platform, the main methods can be divided in different sections:
- functions save and change qubit parameters (``dump``, ``update``)
- functions to coordinate the instruments (``connect``, ``setup``, ``disconnect``)
- a unique interface to execute experiments (``execute``)
- setters and getters of channel/qubit parameters (local oscillator parameters, attenuations, gain and biases)

The idea of the ``Platform`` is to serve as the only object exposed to the user, so that we can deploy experiments, without any need of going into the low-level instrument-specific code.

Expand Down Expand Up @@ -360,9 +359,7 @@ Sweeper objects in Qibolab are characterized by a :class:`qibolab.sweeper.Parame
--

- Frequency
- Attenuation
- Gain
- Bias
- Offset

The first group includes parameters of the pulses, while the second group includes parameters of channels.

Expand Down Expand Up @@ -435,15 +432,15 @@ For example:
sequence.append((qubit.probe.name, Delay(duration=sequence.duration)))
sequence.concatenate(natives.MZ.create_sequence())

f0 = platform.config(str(qubit.drive.name)).frequency
sweeper_freq = Sweeper(
parameter=Parameter.frequency,
values=platform.config(str(qubit.drive.name)).frequency
+ np.arange(-100_000, +100_000, 10_000),
range=(f0 - 100_000, f0 + 100_000, 10_000),
channels=[qubit.drive.name],
)
sweeper_amp = Sweeper(
parameter=Parameter.amplitude,
values=np.arange(0, 0.43, 0.3),
range=(0, 0.43, 0.3),
pulses=[next(iter(sequence.channel(qubit.drive.name)))],
)

Expand Down Expand Up @@ -553,16 +550,15 @@ The shape of the values of an integreted acquisition with 2 sweepers will be:

.. testcode:: python

f0 = platform.config(str(qubit.drive.name)).frequency
sweeper1 = Sweeper(
parameter=Parameter.frequency,
values=platform.config(str(qubit.drive.name)).frequency
+ np.arange(-100_000, +100_000, 1),
range=(f0 - 100_000, f0 + 100_000, 1),
channels=[qubit.drive.name],
)
sweeper2 = Sweeper(
parameter=Parameter.frequency,
values=platform.config(str(qubit.drive.name)).frequency
+ np.arange(-200_000, +200_000, 1),
range=(f0 - 200_000, f0 + 200_000, 1),
channels=[qubit.probe.name],
)
shape = (options.nshots, len(sweeper1.values), len(sweeper2.values))
Expand Down
4 changes: 2 additions & 2 deletions doc/source/tutorials/calibration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ around the pre-defined frequency.
f0 = platform.config(str(qubit.probe.name)).frequency
sweeper = Sweeper(
parameter=Parameter.frequency,
values=f0 + np.arange(-2e8, +2e8, 1e6),
range=(f0 - 2e8, f0 + 2e8, 1e6),
channels=[qubit.probe.name],
)

Expand Down Expand Up @@ -144,7 +144,7 @@ complex pulse sequence. Therefore with start with that:
f0 = platform.config(str(qubit.probe.name)).frequency
sweeper = Sweeper(
parameter=Parameter.frequency,
values=f0 + np.arange(-2e8, +2e8, 1e6),
range=(f0 - 2e8, f0 + 2e8, 1e6),
channels=[qubit.drive.name],
)

Expand Down
4 changes: 2 additions & 2 deletions doc/source/tutorials/lab.rst
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ a two-qubit system:
"frequency": 5800563000
},
"0/flux": {
"bias": 0.0
"offset": 0.0
},
"0/probe": {
"frequency": 7453265000
Expand Down Expand Up @@ -431,7 +431,7 @@ we need the following changes to the previous runcard:
{
"components": {
"flux_coupler_01": {
"bias": 0.12
"offset": 0.12
}
},
"native_gates": {
Expand Down
2 changes: 1 addition & 1 deletion src/qibolab/instruments/qm/program/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def _process_sweeper(sweeper: Sweeper):
variable = declare(int) if parameter in INT_TYPE else declare(fixed)
values = sweeper.values
if parameter in NORMALIZERS:
values = NORMALIZERS[parameter](sweeper.values)
values = NORMALIZERS[parameter](values)

return variable, values

Expand Down
4 changes: 2 additions & 2 deletions src/qibolab/instruments/qm/program/sweepers.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _relative_phase(
args.parameters[operation(pulse)].phase = variable


def _bias(
def _offset(
channels: list[Channel],
values: npt.NDArray,
variable: _Variable,
Expand Down Expand Up @@ -178,6 +178,6 @@ def normalize_duration(values):
Parameter.duration: _duration,
Parameter.duration_interpolated: _duration_interpolated,
Parameter.relative_phase: _relative_phase,
Parameter.bias: _bias,
Parameter.offset: _offset,
}
"""Methods that return part of QUA program to be used inside the loop."""
2 changes: 1 addition & 1 deletion src/qibolab/instruments/zhinst/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ def set_instrument_nodes_for_nt_sweep(
sweeper
):
channel_node_path = self.get_channel_node_path(ch)
if param is Parameter.bias:
if param is Parameter.offset:
offset_node_path = f"{channel_node_path}/offset"
exp.set_node(path=offset_node_path, value=sweep_param)

Expand Down
4 changes: 2 additions & 2 deletions src/qibolab/instruments/zhinst/sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def classify_sweepers(
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 (
if sweeper.parameter is Parameter.offset or (
sweeper.parameter is Parameter.amplitude
# FIXME:
# and sweeper.pulses[0].type is PulseType.READOUT
Expand Down Expand Up @@ -80,7 +80,7 @@ def __init__(

for ch in sweeper.channels or []:
logical_channel = channels[ch].logical_channel
if sweeper.parameter is Parameter.bias:
if sweeper.parameter is Parameter.offset:
sweep_param = laboneq.SweepParameter(
values=sweeper.values + configs[logical_channel.name].offset
)
Expand Down
9 changes: 7 additions & 2 deletions src/qibolab/platform/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,14 @@ def execute(
qubit = platform.qubits[0]
natives = platform.natives.single_qubit[0]
sequence = natives.MZ.create_sequence()
parameter = Parameter.frequency
parameter_range = np.random.randint(10, size=10)
sweeper = [Sweeper(parameter, parameter_range, channels=[qubit.probe.name])]
sweeper = [
Sweeper(
parameter=Parameter.frequency,
values=parameter_range,
channels=[qubit.probe.name],
)
]
platform.execute([sequence], ExecutionParameters(), [sweeper])
"""
if sweepers is None:
Expand Down
99 changes: 51 additions & 48 deletions src/qibolab/sweeper.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
from dataclasses import dataclass
from enum import Enum, auto
from typing import Optional
from functools import cache
from typing import Any, Optional

import numpy as np
import numpy.typing as npt
from pydantic import model_validator

from .identifier import ChannelId
from .pulses import Pulse
from .serialize import Model

_PULSE = "pulse"
_CHANNEL = "channel"


class Parameter(Enum):
"""Sweeping parameters."""

frequency = auto()
amplitude = auto()
duration = auto()
duration_interpolated = auto()
relative_phase = auto()
frequency = (auto(), _CHANNEL)
amplitude = (auto(), _PULSE)
duration = (auto(), _PULSE)
duration_interpolated = (auto(), _PULSE)
relative_phase = (auto(), _PULSE)
offset = (auto(), _CHANNEL)

attenuation = auto()
gain = auto()
bias = auto()
lo_frequency = auto()
@classmethod
@cache
def channels(cls) -> set["Parameter"]:
"""Set of parameters to be swept on the channel."""
return {p for p in cls if p.value[1] == _CHANNEL}


FREQUENCY = Parameter.frequency
AMPLITUDE = Parameter.amplitude
DURATION = Parameter.duration
DURATION_INTERPOLATED = Parameter.duration_interpolated
RELATIVE_PHASE = Parameter.relative_phase
ATTENUATION = Parameter.attenuation
GAIN = Parameter.gain
BIAS = Parameter.bias
_Field = tuple[Any, str]


ChannelParameter = {
Parameter.frequency,
Parameter.bias,
Parameter.attenuation,
Parameter.gain,
}
def _alternative_fields(a: _Field, b: _Field):
if (a[0] is None) == (b[0] is None):
raise ValueError(
f"Either '{a[1]}' or '{b[1]}' needs to be provided, and only one of them."
)


@dataclass
class Sweeper:
class Sweeper(Model):
"""Data structure for Sweeper object.

This object is passed as an argument to the method :func:`qibolab.platforms.platform.Platform.execute`
Expand All @@ -60,44 +62,45 @@ class Sweeper:
qubit = platform.qubits[0]
natives = platform.natives.single_qubit[0]
sequence = natives.MZ.create_sequence()
parameter = Parameter.frequency
parameter_range = np.random.randint(10, size=10)
sweeper = Sweeper(parameter, parameter_range, channels=[qubit.probe.name])
sweeper = Sweeper(
parameter=Parameter.frequency, values=parameter_range, channels=[qubit.probe.name]
)
platform.execute([sequence], ExecutionParameters(), [[sweeper]])

Args:
parameter: parameter to be swept, possible choices are frequency, attenuation, amplitude, current and gain.
values: sweep range. If the parameter of the sweep is a pulse parameter, if the sweeper type is not ABSOLUTE, the base value
will be taken from the runcard pulse parameters. If the sweep parameter is Bias, the base value will be the sweetspot of the qubits.
values: array of parameter values to sweep over.
range: tuple of ``(start, stop, step)`` to sweep over the array ``np.arange(start, stop, step)``.
Can be provided instead of ``values`` for more efficient sweeps on some instruments.
pulses : list of `qibolab.pulses.Pulse` to be swept.
channels: list of channel names for which the parameter should be swept.
type: can be ABSOLUTE (the sweeper range is swept directly),
FACTOR (sweeper values are multiplied by base value), OFFSET (sweeper values are added
to base value)
"""

parameter: Parameter
values: npt.NDArray
pulses: Optional[list] = None
channels: Optional[list] = None
values: Optional[npt.NDArray] = None
range: Optional[tuple[float, float, float]] = None
pulses: Optional[list[Pulse]] = None
channels: Optional[list[ChannelId]] = None

def __post_init__(self):
if self.pulses is not None and self.channels is not None:
raise ValueError(
"Cannot create a sweeper by using both pulses and channels."
)
if self.pulses is not None and self.parameter in ChannelParameter:
@model_validator(mode="after")
def check_values(self):
_alternative_fields((self.pulses, "pulses"), (self.channels, "channels"))
_alternative_fields((self.range, "range"), (self.values, "values"))

if self.pulses is not None and self.parameter in Parameter.channels():
raise ValueError(
f"Cannot create a sweeper for {self.parameter} without specifying channels."
)
if self.parameter not in ChannelParameter and (self.channels is not None):
if self.parameter not in Parameter.channels() and (self.channels is not None):
raise ValueError(
f"Cannot create a sweeper for {self.parameter} without specifying pulses."
)
if self.pulses is None and self.channels is None:
raise ValueError(
"Cannot create a sweeper without specifying pulses or channels."
)

if self.range is not None:
object.__setattr__(self, "values", np.arange(*self.range))

return self


ParallelSweepers = list[Sweeper]
Expand Down
9 changes: 6 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,16 @@ def wrapped(
sequence.concatenate(qd_seq)
sequence.concatenate(probe_seq)
if sweepers is None:
amp_values = np.arange(0.01, 0.06, 0.01)
freq_values = np.arange(-4e6, 4e6, 1e6)
sweeper1 = Sweeper(
Parameter.bias, amp_values, channels=[qubit.flux.name]
parameter=Parameter.offset,
range=(0.01, 0.06, 0.01),
channels=[qubit.flux.name],
)
sweeper2 = Sweeper(
Parameter.amplitude, freq_values, pulses=[probe_pulse]
parameter=Parameter.amplitude,
values=freq_values,
pulses=[probe_pulse],
)
sweepers = [[sweeper1], [sweeper2]]
if target is None:
Expand Down
Loading