From a9b70bc5405b6868693f09b0ee66fdb4fe42792c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:11:46 +0300 Subject: [PATCH 01/79] fix: make frequency and offset sweepers absolute --- .../instruments/qm/program/sweepers.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 91ac3cdc7..9b0f345c1 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -1,11 +1,9 @@ -import math from typing import Optional import numpy as np import numpy.typing as npt from qibo.config import raise_error from qm import qua -from qm.qua import declare, fixed from qm.qua._dsl import _Variable # for type declaration only from qibolab.components import Channel, Config @@ -56,16 +54,14 @@ def _frequency( for channel in channels: name = str(channel.name) lo_frequency = configs[channel.lo].frequency - # convert to IF frequency for readout and drive pulses - f0 = math.floor(configs[name].frequency - lo_frequency) # check if sweep is within the supported bandwidth [-400, 400] MHz - max_freq = maximum_sweep_value(values, f0) + max_freq = maximum_sweep_value(values, -lo_frequency) if max_freq > 4e8: raise_error( ValueError, f"Frequency {max_freq} for channel {name} is beyond instrument bandwidth.", ) - qua.update_frequency(name, variable + f0) + qua.update_frequency(name, variable - lo_frequency) def _amplitude( @@ -107,16 +103,14 @@ def _offset( ): for channel in channels: name = str(channel.name) - offset = configs[name].offset - max_value = maximum_sweep_value(values, offset) + max_value = maximum_sweep_value(values, 0) check_max_offset(max_value, MAX_OFFSET) - b0 = declare(fixed, value=offset) - with qua.if_((variable + b0) >= 0.49): - qua.set_dc_offset(f"flux{name}", "single", 0.49) - with qua.elif_((variable + b0) <= -0.49): - qua.set_dc_offset(f"flux{name}", "single", -0.49) + with qua.if_(variable >= MAX_OFFSET): + qua.set_dc_offset(name, "single", MAX_OFFSET) + with qua.elif_(variable <= -MAX_OFFSET): + qua.set_dc_offset(name, "single", -MAX_OFFSET) with qua.else_(): - qua.set_dc_offset(f"flux{name}", "single", (variable + b0)) + qua.set_dc_offset(name, "single", variable) def _duration( From b79e0f0a8f75600ca89d9e6c3f60d68eb31d964d Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:12:59 +0300 Subject: [PATCH 02/79] chore: drop check_max_offset --- src/qibolab/instruments/qm/program/sweepers.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 9b0f345c1..396128166 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -1,5 +1,3 @@ -from typing import Optional - import numpy as np import numpy.typing as npt from qibo.config import raise_error @@ -32,18 +30,6 @@ def maximum_sweep_value(values: npt.NDArray, value0: npt.NDArray) -> float: return max(abs(min(values) + value0), abs(max(values) + value0)) -def check_max_offset(offset: Optional[float], max_offset: float = MAX_OFFSET): - """Checks if a given offset value exceeds the maximum supported offset. - - This is to avoid sending high currents that could damage lab - equipment such as amplifiers. - """ - if max_offset is not None and abs(offset) > max_offset: - raise_error( - ValueError, f"{offset} exceeds the maximum allowed offset {max_offset}." - ) - - def _frequency( channels: list[Channel], values: npt.NDArray, @@ -104,7 +90,6 @@ def _offset( for channel in channels: name = str(channel.name) max_value = maximum_sweep_value(values, 0) - check_max_offset(max_value, MAX_OFFSET) with qua.if_(variable >= MAX_OFFSET): qua.set_dc_offset(name, "single", MAX_OFFSET) with qua.elif_(variable <= -MAX_OFFSET): From 284072a52f3474b0ed058b01d503aeef30cdeb92 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:14:58 +0300 Subject: [PATCH 03/79] fix: drop max value calculation method --- src/qibolab/instruments/qm/program/sweepers.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 396128166..f7d21afdd 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -15,21 +15,6 @@ """Maximum voltage supported by Quantum Machines OPX+ instrument in volts.""" -def maximum_sweep_value(values: npt.NDArray, value0: npt.NDArray) -> float: - """Calculates maximum value that is reached during a sweep. - - Useful to check whether a sweep exceeds the range of allowed values. - Note that both the array of values we sweep and the center value can - be negative, so we need to make sure that the maximum absolute value - is within range. - - Args: - values (np.ndarray): Array of values we will sweep over. - value0 (float, int): Center value of the sweep. - """ - return max(abs(min(values) + value0), abs(max(values) + value0)) - - def _frequency( channels: list[Channel], values: npt.NDArray, @@ -41,7 +26,7 @@ def _frequency( name = str(channel.name) lo_frequency = configs[channel.lo].frequency # check if sweep is within the supported bandwidth [-400, 400] MHz - max_freq = maximum_sweep_value(values, -lo_frequency) + max_freq = np.max(np.abs(values - lo_frequency)) if max_freq > 4e8: raise_error( ValueError, @@ -89,7 +74,6 @@ def _offset( ): for channel in channels: name = str(channel.name) - max_value = maximum_sweep_value(values, 0) with qua.if_(variable >= MAX_OFFSET): qua.set_dc_offset(name, "single", MAX_OFFSET) with qua.elif_(variable <= -MAX_OFFSET): From e4a8c10ea661a5b09fcfc221c7042f0eca2b5b84 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 29 Aug 2024 19:26:48 +0300 Subject: [PATCH 04/79] fix: make amplitude sweeper absolute --- src/qibolab/instruments/qm/controller.py | 41 ++++++++++++++++--- .../instruments/qm/program/arguments.py | 5 ++- .../instruments/qm/program/instructions.py | 2 +- .../instruments/qm/program/sweepers.py | 35 +++++++++++----- src/qibolab/sweeper.py | 7 +++- tests/conftest.py | 4 +- tests/test_sweeper.py | 8 ++++ 7 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 112ace50a..1d0345963 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -23,6 +23,7 @@ from .components import QmChannel from .config import SAMPLING_RATE, QmConfig, operation from .program import ExecutionArguments, create_acquisition, program +from .program.sweepers import sweeper_amplitude OCTAVE_ADDRESS_OFFSET = 11000 """Offset to be added to Octave addresses, because they must be 11xxx, where @@ -95,9 +96,15 @@ def fetch_results(result, acquisitions): } -def find_duration_sweepers(sweepers: list[ParallelSweepers]) -> list[Sweeper]: - """Find duration sweepers in order to register multiple pulses.""" - return [s for ps in sweepers for s in ps if s.parameter is Parameter.duration] +def find_sweepers( + sweepers: list[ParallelSweepers], parameter: Parameter +) -> list[Sweeper]: + """Find sweepers of given parameter in order to register specific pulses. + + Duration and amplitude sweepers may require registering additional pulses + in the QM ``config``. + """ + return [s for ps in sweepers for s in ps if s.parameter is parameter] @dataclass @@ -309,8 +316,10 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): def register_duration_sweeper_pulses( self, args: ExecutionArguments, sweeper: Sweeper ): - """Register pulse with many different durations, in order to sweep - duration.""" + """Register pulse with many different durations. + + Needed when sweeping duration. + """ for pulse in sweeper.pulses: if isinstance(pulse, (Align, Delay)): continue @@ -323,6 +332,24 @@ def register_duration_sweeper_pulses( sweep_op = self.register_pulse(channel, sweep_pulse) args.parameters[op].pulses.append((value, sweep_op)) + def register_amplitude_sweeper_pulses( + self, args: ExecutionArguments, sweeper: Sweeper + ): + """Register pulse with different amplitude. + + Needed when sweeping amplitude and the original amplitude is not + sufficient to reach all the sweeper values. + """ + new_op = None + amplitude = sweeper_amplitude(sweeper.values) + for pulse in sweeper.pulses: + new_pulse = pulse.model_copy(update={"amplitude": amplitude}) + channel_ids = args.sequence.pulse_channels(pulse.id) + channel = self.channels[str(channel_ids[0])].logical_channel + args.parameters[operation(pulse)].amplitude_pulse = self.register_pulse( + channel, new_pulse + ) + def register_acquisitions( self, configs: dict[str, Config], @@ -410,8 +437,10 @@ def play( args = ExecutionArguments(sequence, acquisitions, options.relaxation_time) - for sweeper in find_duration_sweepers(sweepers): + for sweeper in find_sweepers(sweepers, Parameter.duration): self.register_duration_sweeper_pulses(args, sweeper) + for sweeper in find_sweepers(sweepers, Parameter.amplitude): + self.register_amplitude_sweeper_pulses(args, sweeper) experiment = program(configs, args, options, sweepers) diff --git a/src/qibolab/instruments/qm/program/arguments.py b/src/qibolab/instruments/qm/program/arguments.py index 571e1a3cc..b02de5a46 100644 --- a/src/qibolab/instruments/qm/program/arguments.py +++ b/src/qibolab/instruments/qm/program/arguments.py @@ -13,9 +13,12 @@ class Parameters: """Container of swept QUA variables.""" - duration: Optional[_Variable] = None amplitude: Optional[_Variable] = None + amplitude_pulse: Optional[str] = None + phase: Optional[_Variable] = None + + duration: Optional[_Variable] = None pulses: list[tuple[float, str]] = field(default_factory=list) interpolated: bool = False diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index 115294dfe..e09f9de80 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -49,7 +49,7 @@ def _play_single_waveform( acquisition: Optional[Acquisition] = None, ): if parameters.amplitude is not None: - op = op * parameters.amplitude + op = parameters.amplitude_pulse * parameters.amplitude if acquisition is not None: acquisition.measure(op) else: diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index f7d21afdd..b897e63af 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -13,6 +13,11 @@ MAX_OFFSET = 0.5 """Maximum voltage supported by Quantum Machines OPX+ instrument in volts.""" +MAX_AMPLITUDE_FACTOR = 1.99 +"""Maximum multiplication factor for ``qua.amp`` used when sweeping amplitude. + +https://docs.quantum-machines.co/1.2.0/docs/API_references/qua/dsl_main/#qm.qua._dsl.amp +""" def _frequency( @@ -42,14 +47,6 @@ def _amplitude( configs: dict[str, Config], args: ExecutionArguments, ): - # TODO: Consider sweeping amplitude without multiplication - if min(values) < -2: - raise_error( - ValueError, "Amplitude sweep values are <-2 which is not supported." - ) - if max(values) > 2: - raise_error(ValueError, "Amplitude sweep values are >2 which is not supported.") - for pulse in pulses: args.parameters[operation(pulse)].amplitude = qua.amp(variable) @@ -106,12 +103,29 @@ def _duration_interpolated( params.interpolated = True -def normalize_phase(values): +def sweeper_amplitude(values: npt.NDArray) -> float: + """Pulse amplitude to be registered in the QM ``config`` when sweeping + amplitude. + + The multiplicative factor used in the ``qua.amp`` command is limited, so we + may need to register a pulse with different amplitude than the original pulse + in the sequence, in order to reach all sweeper values when sweeping amplitude. + """ + return max(abs(values)) / MAX_AMPLITUDE_FACTOR + + +def normalize_amplitude(values: npt.NDArray) -> npt.NDArray: + """Normalize amplitude factor to [-MAX_AMPLITUDE_FACTOR, + MAX_AMPLITUDE_FACTOR].""" + return values / sweeper_amplitude(values) + + +def normalize_phase(values: npt.NDArray) -> npt.NDArray: """Normalize phase from [0, 2pi] to [0, 1].""" return values / (2 * np.pi) -def normalize_duration(values): +def normalize_duration(values: npt.NDArray) -> npt.NDArray: """Convert duration from ns to clock cycles (clock cycle = 4ns).""" if any(values < 16) and not all(values % 4 == 0): raise ValueError( @@ -127,6 +141,7 @@ def normalize_duration(values): """ NORMALIZERS = { + Parameter.amplitude: normalize_amplitude, Parameter.relative_phase: normalize_phase, Parameter.duration_interpolated: normalize_duration, } diff --git a/src/qibolab/sweeper.py b/src/qibolab/sweeper.py index affb5a4e7..37c1a48de 100644 --- a/src/qibolab/sweeper.py +++ b/src/qibolab/sweeper.py @@ -7,7 +7,7 @@ from pydantic import model_validator from .identifier import ChannelId -from .pulses import Pulse +from .pulses import PulseLike from .serialize import Model _PULSE = "pulse" @@ -80,7 +80,7 @@ class Sweeper(Model): parameter: Parameter values: Optional[npt.NDArray] = None range: Optional[tuple[float, float, float]] = None - pulses: Optional[list[Pulse]] = None + pulses: Optional[list[PulseLike]] = None channels: Optional[list[ChannelId]] = None @model_validator(mode="after") @@ -100,6 +100,9 @@ def check_values(self): if self.range is not None: object.__setattr__(self, "values", np.arange(*self.range)) + if self.parameter is Parameter.amplitude and max(abs(self.values)) > 1: + raise ValueError("Amplitude sweeper cannot have values larger than 1.") + return self diff --git a/tests/conftest.py b/tests/conftest.py index 71a85d8b9..d79f28794 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -154,7 +154,7 @@ def wrapped( sequence.concatenate(qd_seq) sequence.concatenate(probe_seq) if sweepers is None: - freq_values = np.arange(-4e6, 4e6, 1e6) + amp_values = np.arange(0, 0.8, 0.1) sweeper1 = Sweeper( parameter=Parameter.offset, range=(0.01, 0.06, 0.01), @@ -162,7 +162,7 @@ def wrapped( ) sweeper2 = Sweeper( parameter=Parameter.amplitude, - values=freq_values, + values=amp_values, pulses=[probe_pulse], ) sweepers = [[sweeper1], [sweeper2]] diff --git a/tests/test_sweeper.py b/tests/test_sweeper.py index db22ab32f..275a29ffd 100644 --- a/tests/test_sweeper.py +++ b/tests/test_sweeper.py @@ -68,3 +68,11 @@ def test_sweeper_errors(): parameter=Parameter.frequency, channels=[channel], ) + with pytest.raises( + ValueError, match="Amplitude sweeper cannot have values larger than 1." + ): + Sweeper( + parameter=Parameter.amplitude, + range=(0, 2, 0.2), + pulses=[pulse], + ) From 7c74f9a6f356093ae53784fe0fbb668473ffef4b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:27:44 +0300 Subject: [PATCH 05/79] fix: amplitude sweeper nested in multi-waveform duration sweeper --- src/qibolab/instruments/qm/controller.py | 26 +++++++++++-------- .../instruments/qm/program/arguments.py | 6 +++-- .../instruments/qm/program/instructions.py | 6 ++--- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 1d0345963..a4640304f 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -324,13 +324,16 @@ def register_duration_sweeper_pulses( if isinstance(pulse, (Align, Delay)): continue - op = operation(pulse) - channel_name = str(args.sequence.pulse_channels(pulse.id)[0]) - channel = self.channels[channel_name].logical_channel + params = args.parameters[operation(pulse)] + channel_ids = args.sequence.pulse_channels(pulse.id) + channel = self.channels[str(channel_ids[0])].logical_channel + original_pulse = ( + pulse if params.amplitude_pulse is None else params.amplitude_pulse + ) for value in sweeper.values: - sweep_pulse = pulse.model_copy(update={"duration": value}) + sweep_pulse = original_pulse.model_copy(update={"duration": value}) sweep_op = self.register_pulse(channel, sweep_pulse) - args.parameters[op].pulses.append((value, sweep_op)) + params.duration_ops.append((value, sweep_op)) def register_amplitude_sweeper_pulses( self, args: ExecutionArguments, sweeper: Sweeper @@ -343,12 +346,13 @@ def register_amplitude_sweeper_pulses( new_op = None amplitude = sweeper_amplitude(sweeper.values) for pulse in sweeper.pulses: - new_pulse = pulse.model_copy(update={"amplitude": amplitude}) channel_ids = args.sequence.pulse_channels(pulse.id) channel = self.channels[str(channel_ids[0])].logical_channel - args.parameters[operation(pulse)].amplitude_pulse = self.register_pulse( - channel, new_pulse - ) + sweep_pulse = pulse.model_copy(update={"amplitude": amplitude}) + + params = args.parameters[operation(pulse)] + params.amplitude_pulse = sweep_pulse + params.amplitude_op = self.register_pulse(channel, sweep_pulse) def register_acquisitions( self, @@ -437,10 +441,10 @@ def play( args = ExecutionArguments(sequence, acquisitions, options.relaxation_time) - for sweeper in find_sweepers(sweepers, Parameter.duration): - self.register_duration_sweeper_pulses(args, sweeper) for sweeper in find_sweepers(sweepers, Parameter.amplitude): self.register_amplitude_sweeper_pulses(args, sweeper) + for sweeper in find_sweepers(sweepers, Parameter.duration): + self.register_duration_sweeper_pulses(args, sweeper) experiment = program(configs, args, options, sweepers) diff --git a/src/qibolab/instruments/qm/program/arguments.py b/src/qibolab/instruments/qm/program/arguments.py index b02de5a46..eaf7cd270 100644 --- a/src/qibolab/instruments/qm/program/arguments.py +++ b/src/qibolab/instruments/qm/program/arguments.py @@ -4,6 +4,7 @@ from qm.qua._dsl import _Variable # for type declaration only +from qibolab.pulses import Pulse from qibolab.sequence import PulseSequence from .acquisition import Acquisitions @@ -14,12 +15,13 @@ class Parameters: """Container of swept QUA variables.""" amplitude: Optional[_Variable] = None - amplitude_pulse: Optional[str] = None + amplitude_pulse: Optional[Pulse] = None + amplitude_op: Optional[str] = None phase: Optional[_Variable] = None duration: Optional[_Variable] = None - pulses: list[tuple[float, str]] = field(default_factory=list) + duration_ops: list[tuple[float, str]] = field(default_factory=list) interpolated: bool = False diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index e09f9de80..64a31cf7e 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -35,7 +35,7 @@ def _delay(pulse: Delay, element: str, parameters: Parameters): def _play_multiple_waveforms(element: str, parameters: Parameters): """Sweeping pulse duration using distinctly uploaded waveforms.""" with qua.switch_(parameters.duration, unsafe=True): - for value, sweep_op in parameters.pulses: + for value, sweep_op in parameters.duration_ops: if parameters.amplitude is not None: sweep_op = sweep_op * parameters.amplitude with qua.case_(value): @@ -49,7 +49,7 @@ def _play_single_waveform( acquisition: Optional[Acquisition] = None, ): if parameters.amplitude is not None: - op = parameters.amplitude_pulse * parameters.amplitude + op = parameters.amplitude_op * parameters.amplitude if acquisition is not None: acquisition.measure(op) else: @@ -65,7 +65,7 @@ def _play( if parameters.phase is not None: qua.frame_rotation_2pi(parameters.phase, element) - if len(parameters.pulses) > 0: + if len(parameters.duration_ops) > 0: _play_multiple_waveforms(element, parameters) else: _play_single_waveform(op, element, parameters, acquisition) From 10227d2ba0514ea23a793eb2cca2ddca9f435242 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:05:21 +0300 Subject: [PATCH 06/79] refactor: simplify QM sweepers --- src/qibolab/instruments/qm/controller.py | 26 ++-- .../instruments/qm/program/instructions.py | 7 +- .../instruments/qm/program/sweepers.py | 118 +++++++----------- 3 files changed, 66 insertions(+), 85 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index a4640304f..3608d0655 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -23,7 +23,7 @@ from .components import QmChannel from .config import SAMPLING_RATE, QmConfig, operation from .program import ExecutionArguments, create_acquisition, program -from .program.sweepers import sweeper_amplitude +from .program.sweepers import check_frequency_bandwidth, sweeper_amplitude OCTAVE_ADDRESS_OFFSET = 11000 """Offset to be added to Octave addresses, because they must be 11xxx, where @@ -397,6 +397,23 @@ def register_acquisitions( return acquisitions + def preprocess_sweeps( + self, + sweepers: list[ParallelSweepers], + configs: dict[str, Config], + args: ExecutionArguments, + ): + """Preprocessing and checks needed before executing some sweeps. + + Amplitude and duration sweeps require registering additional pulses in the QM ``config. + """ + for sweeper in find_sweepers(sweepers, Parameter.frequency): + check_frequency_bandwidth(sweeper.channels, configs, sweeper.values) + for sweeper in find_sweepers(sweepers, Parameter.amplitude): + self.register_amplitude_sweeper_pulses(args, sweeper) + for sweeper in find_sweepers(sweepers, Parameter.duration): + self.register_duration_sweeper_pulses(args, sweeper) + def execute_program(self, program): """Executes an arbitrary program written in QUA language.""" machine = self.manager.open_qm(asdict(self.config)) @@ -440,12 +457,7 @@ def play( acquisitions = self.register_acquisitions(configs, sequence, options) args = ExecutionArguments(sequence, acquisitions, options.relaxation_time) - - for sweeper in find_sweepers(sweepers, Parameter.amplitude): - self.register_amplitude_sweeper_pulses(args, sweeper) - for sweeper in find_sweepers(sweepers, Parameter.duration): - self.register_duration_sweeper_pulses(args, sweeper) - + self.preprocess_sweeps(sweepers, configs, args) experiment = program(configs, args, options, sweepers) if self.script_file_name is not None: diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index 64a31cf7e..3fc11289f 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -148,9 +148,12 @@ def sweep( ): method = SWEEPER_METHODS[sweeper.parameter] if sweeper.pulses is not None: - method(sweeper.pulses, values, variable, configs, args) + for pulse in sweeper.pulses: + params = args.parameters[operation(pulse)] + method(variable, params) else: - method(sweeper.channels, values, variable, configs, args) + for channel in sweeper.channels: + method(variable, channel, configs) sweep(sweepers[1:], configs, args) diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index b897e63af..7be03a4af 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -5,11 +5,9 @@ from qm.qua._dsl import _Variable # for type declaration only from qibolab.components import Channel, Config -from qibolab.pulses import Pulse from qibolab.sweeper import Parameter -from ..config import operation -from .arguments import ExecutionArguments +from .arguments import Parameters MAX_OFFSET = 0.5 """Maximum voltage supported by Quantum Machines OPX+ instrument in volts.""" @@ -18,89 +16,24 @@ https://docs.quantum-machines.co/1.2.0/docs/API_references/qua/dsl_main/#qm.qua._dsl.amp """ +FREQUENCY_BANDWIDTH = 4e8 +"""Quantum Machines OPX+ frequency bandwidth in Hz.""" -def _frequency( - channels: list[Channel], - values: npt.NDArray, - variable: _Variable, - configs: dict[str, Config], - args: ExecutionArguments, +def check_frequency_bandwidth( + channels: list[Channel], configs: dict[str, Channel], values: npt.NDArray ): + """Check if frequency sweep is within the supported instrument bandwidth + [-400, 400] MHz.""" for channel in channels: name = str(channel.name) lo_frequency = configs[channel.lo].frequency - # check if sweep is within the supported bandwidth [-400, 400] MHz - max_freq = np.max(np.abs(values - lo_frequency)) - if max_freq > 4e8: + max_freq = max(abs(values - lo_frequency)) + if max_freq > FREQUENCY_BANDWIDTH: raise_error( ValueError, f"Frequency {max_freq} for channel {name} is beyond instrument bandwidth.", ) - qua.update_frequency(name, variable - lo_frequency) - - -def _amplitude( - pulses: list[Pulse], - values: npt.NDArray, - variable: _Variable, - configs: dict[str, Config], - args: ExecutionArguments, -): - for pulse in pulses: - args.parameters[operation(pulse)].amplitude = qua.amp(variable) - - -def _relative_phase( - pulses: list[Pulse], - values: npt.NDArray, - variable: _Variable, - configs: dict[str, Config], - args: ExecutionArguments, -): - for pulse in pulses: - args.parameters[operation(pulse)].phase = variable - - -def _offset( - channels: list[Channel], - values: npt.NDArray, - variable: _Variable, - configs: dict[str, Config], - args: ExecutionArguments, -): - for channel in channels: - name = str(channel.name) - with qua.if_(variable >= MAX_OFFSET): - qua.set_dc_offset(name, "single", MAX_OFFSET) - with qua.elif_(variable <= -MAX_OFFSET): - qua.set_dc_offset(name, "single", -MAX_OFFSET) - with qua.else_(): - qua.set_dc_offset(name, "single", variable) - - -def _duration( - pulses: list[Pulse], - values: npt.NDArray, - variable: _Variable, - configs: dict[str, Config], - args: ExecutionArguments, -): - for pulse in pulses: - args.parameters[operation(pulse)].duration = variable - - -def _duration_interpolated( - pulses: list[Pulse], - values: npt.NDArray, - variable: _Variable, - configs: dict[str, Config], - args: ExecutionArguments, -): - for pulse in pulses: - params = args.parameters[operation(pulse)] - params.duration = variable - params.interpolated = True def sweeper_amplitude(values: npt.NDArray) -> float: @@ -134,6 +67,39 @@ def normalize_duration(values: npt.NDArray) -> npt.NDArray: return (values // 4).astype(int) +def _amplitude(variable: _Variable, parameters: Parameters): + parameters.amplitude = qua.amp(variable) + + +def _relative_phase(variable: _Variable, parameters: Parameters): + parameters.phase = variable + + +def _duration(variable: _Variable, parameters: Parameters): + parameters.duration = variable + + +def _duration_interpolated(variable: _Variable, parameters: Parameters): + parameters.duration = variable + parameters.interpolated = True + + +def _offset(variable: _Variable, channel: Channel, configs: dict[str, Config]): + name = str(channel.name) + with qua.if_(variable >= MAX_OFFSET): + qua.set_dc_offset(name, "single", MAX_OFFSET) + with qua.elif_(variable <= -MAX_OFFSET): + qua.set_dc_offset(name, "single", -MAX_OFFSET) + with qua.else_(): + qua.set_dc_offset(name, "single", variable) + + +def _frequency(variable: _Variable, channel: Channel, configs: dict[str, Config]): + name = str(channel.name) + lo_frequency = configs[channel.lo].frequency + qua.update_frequency(name, variable - lo_frequency) + + INT_TYPE = {Parameter.frequency, Parameter.duration, Parameter.duration_interpolated} """Sweeper parameters for which we need ``int`` variable type. From f6a86b71396c477b448779bcb6d942d8766fa1ae Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:14:18 +0300 Subject: [PATCH 07/79] fix: docstring --- src/qibolab/instruments/qm/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 3608d0655..8d536ee3c 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -340,8 +340,8 @@ def register_amplitude_sweeper_pulses( ): """Register pulse with different amplitude. - Needed when sweeping amplitude and the original amplitude is not - sufficient to reach all the sweeper values. + Needed when sweeping amplitude because the original amplitude + may not sufficient to reach all the sweeper values. """ new_op = None amplitude = sweeper_amplitude(sweeper.values) From 6b01a434f3c436a947fbaf8d1c1d5749b75bb673 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 30 Aug 2024 16:05:16 +0300 Subject: [PATCH 08/79] Update src/qibolab/sweeper.py Co-authored-by: Alessandro Candido --- src/qibolab/sweeper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibolab/sweeper.py b/src/qibolab/sweeper.py index 37c1a48de..3b4a849b8 100644 --- a/src/qibolab/sweeper.py +++ b/src/qibolab/sweeper.py @@ -101,7 +101,7 @@ def check_values(self): object.__setattr__(self, "values", np.arange(*self.range)) if self.parameter is Parameter.amplitude and max(abs(self.values)) > 1: - raise ValueError("Amplitude sweeper cannot have values larger than 1.") + raise ValueError("Amplitude sweeper cannot have absolute values larger than 1.") return self From 1fb01905db3777e41c9ce98e414756126ad23c72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:05:27 +0000 Subject: [PATCH 09/79] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/qibolab/sweeper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/qibolab/sweeper.py b/src/qibolab/sweeper.py index 3b4a849b8..20587e806 100644 --- a/src/qibolab/sweeper.py +++ b/src/qibolab/sweeper.py @@ -101,7 +101,9 @@ def check_values(self): object.__setattr__(self, "values", np.arange(*self.range)) if self.parameter is Parameter.amplitude and max(abs(self.values)) > 1: - raise ValueError("Amplitude sweeper cannot have absolute values larger than 1.") + raise ValueError( + "Amplitude sweeper cannot have absolute values larger than 1." + ) return self From cef71e441627e643b3d237d843087a1c2fa47082 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 30 Aug 2024 16:11:22 +0300 Subject: [PATCH 10/79] fix: error match --- tests/test_sweeper.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_sweeper.py b/tests/test_sweeper.py index 275a29ffd..ff02cd245 100644 --- a/tests/test_sweeper.py +++ b/tests/test_sweeper.py @@ -68,9 +68,7 @@ def test_sweeper_errors(): parameter=Parameter.frequency, channels=[channel], ) - with pytest.raises( - ValueError, match="Amplitude sweeper cannot have values larger than 1." - ): + with pytest.raises(ValueError, match="Amplitude"): Sweeper( parameter=Parameter.amplitude, range=(0, 2, 0.2), From bbfea830b184bb55393f60d48ba7f2e8169deb6d Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 13:23:39 +0200 Subject: [PATCH 11/79] feat!: Make qubit holding references, not actual channels --- src/qibolab/qubits.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 17a419eca..960e86b9f 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -3,9 +3,8 @@ from pydantic import ConfigDict -from .components import AcquireChannel, DcChannel, IqChannel from .components.channels import Channel -from .identifier import ChannelType, QubitId +from .identifier import ChannelId, ChannelType, QubitId from .serialize import Model @@ -29,12 +28,11 @@ class Qubit(Model): name: QubitId - probe: Optional[IqChannel] = None - acquisition: Optional[AcquireChannel] = None - drive: Optional[IqChannel] = None - drive12: Optional[IqChannel] = None - drive_cross: Optional[dict[QubitId, IqChannel]] = None - flux: Optional[DcChannel] = None + probe: Optional[ChannelId] = None + acquisition: Optional[ChannelId] = None + drive: Optional[ChannelId] = None + drive_qudits: Optional[dict[str, ChannelId]] = None + flux: Optional[ChannelId] = None @property def channels(self) -> Iterable[Channel]: From bf3d8973415abac70428dc0b027a87cf844bd198 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 13:34:44 +0200 Subject: [PATCH 12/79] feat!: Remove internal references to objects' own names Names are the way an object is known in a certain context, so it is not an intrinsic property of the object itself. It should be up to the retriever to preserve the name even after obtaining the object, if still needed. --- src/qibolab/components/channels.py | 4 +--- src/qibolab/qubits.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/qibolab/components/channels.py b/src/qibolab/components/channels.py index d42469ed5..e385ad57f 100644 --- a/src/qibolab/components/channels.py +++ b/src/qibolab/components/channels.py @@ -20,15 +20,13 @@ from typing import Optional -from qibolab.identifier import ChannelId from qibolab.serialize import Model __all__ = ["Channel", "DcChannel", "IqChannel", "AcquireChannel"] class Channel(Model): - name: ChannelId - """Name of the channel.""" + """Channel to communicate with the qubit.""" class DcChannel(Channel): diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 960e86b9f..5742fc8c2 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -26,8 +26,6 @@ class Qubit(Model): model_config = ConfigDict(frozen=False) - name: QubitId - probe: Optional[ChannelId] = None acquisition: Optional[ChannelId] = None drive: Optional[ChannelId] = None From 7bc066fa3ef4c1c3deaa913c219d0b51934874fc Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 13:36:58 +0200 Subject: [PATCH 13/79] feat!: Remove obsolete mixer frequencies accessor --- src/qibolab/qubits.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 5742fc8c2..232fac002 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -38,19 +38,3 @@ def channels(self) -> Iterable[Channel]: channel = getattr(self, ct.value) if channel is not None: yield channel - - @property - def mixer_frequencies(self): - """Get local oscillator and intermediate frequencies of native gates. - - Assumes RF = LO + IF. - """ - freqs = {} - for name in self.native_gates.model_fields: - native = getattr(self.native_gates, name) - if native is not None: - channel_type = native.pulse_type.name.lower() - _lo = getattr(self, channel_type).lo_frequency - _if = native.frequency - _lo - freqs[name] = _lo, _if - return freqs From e05f6645799c689d942d78edf77d6466b4fb7fa4 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 13:37:38 +0200 Subject: [PATCH 14/79] feat!: Remove obsolete flux constructor for pulses --- src/qibolab/pulses/pulse.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/qibolab/pulses/pulse.py b/src/qibolab/pulses/pulse.py index ce9bc2f33..c5b58ebce 100644 --- a/src/qibolab/pulses/pulse.py +++ b/src/qibolab/pulses/pulse.py @@ -42,16 +42,6 @@ class Pulse(_PulseLike): relative_phase: float = 0.0 """Relative phase of the pulse, in radians.""" - @classmethod - def flux(cls, **kwargs): - """Construct a flux pulse. - - It provides a simplified syntax for the :class:`Pulse` constructor, by applying - suitable defaults. - """ - kwargs["relative_phase"] = 0 - return cls(**kwargs) - def i(self, sampling_rate: float) -> Waveform: """The envelope waveform of the i component of the pulse.""" samples = int(self.duration * sampling_rate) From 91bc35d602264627485ad2c43eaed6b6d37afb1c Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 13:38:40 +0200 Subject: [PATCH 15/79] test: Remove flux constructor tests --- tests/pulses/test_pulse.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/pulses/test_pulse.py b/tests/pulses/test_pulse.py index 0c6408b5e..c0b35e7d7 100644 --- a/tests/pulses/test_pulse.py +++ b/tests/pulses/test_pulse.py @@ -1,20 +1,12 @@ """Tests ``pulses.py``.""" import numpy as np -import pytest +from pytest import approx, raises from qibolab.pulses import Acquisition, Custom, Pulse, Rectangular, VirtualZ from qibolab.pulses.pulse import _Readout -def test_flux(): - p = Pulse.flux(duration=5, amplitude=0.9, envelope=Rectangular()) - assert p.relative_phase == 0 - - p1 = Pulse.flux(duration=5, amplitude=0.9, relative_phase=1, envelope=Rectangular()) - assert p1.relative_phase == 0 - - def test_virtual_z(): vz = VirtualZ(phase=-0.3) assert vz.duration == 0 @@ -31,10 +23,10 @@ def test_readout(): def test_envelope_waveform_i_q(): d = 1000 p = Pulse(duration=d, amplitude=1, envelope=Rectangular()) - assert pytest.approx(p.i(1)) == np.ones(d) - assert pytest.approx(p.i(2)) == np.ones(2 * d) - assert pytest.approx(p.q(1)) == np.zeros(d) - assert pytest.approx(p.envelopes(1)) == np.stack([np.ones(d), np.zeros(d)]) + assert approx(p.i(1)) == np.ones(d) + assert approx(p.i(2)) == np.ones(2 * d) + assert approx(p.q(1)) == np.zeros(d) + assert approx(p.envelopes(1)) == np.stack([np.ones(d), np.zeros(d)]) envelope_i = np.cos(np.arange(0, 10, 0.01)) envelope_q = np.sin(np.arange(0, 10, 0.01)) @@ -42,7 +34,7 @@ def test_envelope_waveform_i_q(): pulse = Pulse(duration=1000, amplitude=1, relative_phase=0, envelope=Rectangular()) custom_shape_pulse = custom_shape_pulse.model_copy(update={"i_": pulse.i(1)}) - with pytest.raises(ValueError): + with raises(ValueError): custom_shape_pulse.i(samples=10) - with pytest.raises(ValueError): + with raises(ValueError): custom_shape_pulse.q(samples=10) From 758c9e39a8b86c85008c496bac389c982b3e6db1 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 15:25:30 +0200 Subject: [PATCH 16/79] test: Fix pulses plots tests --- tests/pulses/test_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pulses/test_plot.py b/tests/pulses/test_plot.py index baf542b91..2f904d2a5 100644 --- a/tests/pulses/test_plot.py +++ b/tests/pulses/test_plot.py @@ -40,12 +40,12 @@ def test_plot_functions(): envelope=Drag(rel_sigma=0.2, beta=2), relative_phase=0, ) - p3 = Pulse.flux( + p3 = Pulse( duration=40, amplitude=0.9, envelope=Iir(a=np.array([-0.5, 2]), b=np.array([1]), target=Rectangular()), ) - p4 = Pulse.flux(duration=40, amplitude=0.9, envelope=Snz(t_idling=10)) + p4 = Pulse(duration=40, amplitude=0.9, envelope=Snz(t_idling=10)) p5 = Pulse( duration=40, amplitude=0.9, From 2e20f7d305e14ebe001d5d13644b5103adf88378 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 15:31:43 +0200 Subject: [PATCH 17/79] feat!: Drop structured channel IDs --- src/qibolab/identifier.py | 53 +++------------------------------------ 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/src/qibolab/identifier.py b/src/qibolab/identifier.py index 4047c8a9e..6a077aa4d 100644 --- a/src/qibolab/identifier.py +++ b/src/qibolab/identifier.py @@ -1,16 +1,7 @@ from enum import Enum -from typing import Annotated, Optional, Union +from typing import Annotated, Union -from pydantic import ( - BeforeValidator, - Field, - PlainSerializer, - TypeAdapter, - model_serializer, - model_validator, -) - -from .serialize import Model +from pydantic import BeforeValidator, Field, PlainSerializer QubitId = Annotated[Union[int, str], Field(union_mode="left_to_right")] """Type for qubit names.""" @@ -34,47 +25,11 @@ class ChannelType(str, Enum): PROBE = "probe" ACQUISITION = "acquisition" DRIVE = "drive" - DRIVE12 = "drive12" - DRIVE_CROSS = "drive_cross" FLUX = "flux" def __str__(self) -> str: return str(self.value) -_adapted_qubit = TypeAdapter(QubitId) - - -class ChannelId(Model): - """Unique identifier for a channel.""" - - qubit: QubitId - channel_type: ChannelType - cross: Optional[str] - - @model_validator(mode="before") - @classmethod - def _load(cls, ch: str) -> dict: - elements = ch.split("/") - # TODO: replace with pattern matching, once py3.9 will be abandoned - if len(elements) > 3: - raise ValueError() - q = _adapted_qubit.validate_python(elements[0]) - ct = ChannelType(elements[1]) - assert len(elements) == 2 or ct is ChannelType.DRIVE_CROSS - dc = elements[2] if len(elements) == 3 else None - return dict(qubit=q, channel_type=ct, cross=dc) - - @classmethod - def load(cls, value: str): - """Unpack from string.""" - return cls.model_validate(value) - - def __str__(self): - """Represent as its joint components.""" - return "/".join(str(el[1]) for el in self if el[1] is not None) - - @model_serializer - def _dump(self) -> str: - """Prepare for serialization.""" - return str(self) +ChannelId = str +"""Unique identifier for a channel.""" From 2adb673f29531a2f37ba5e68b2d8b66ce0a4124a Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 15:58:48 +0200 Subject: [PATCH 18/79] feat!: Drop readout automated grouping --- src/qibolab/pulses/__init__.py | 2 +- src/qibolab/pulses/pulse.py | 4 +-- src/qibolab/sequence.py | 42 ++++------------------- tests/test_sequence.py | 61 +++++++++++----------------------- 4 files changed, 28 insertions(+), 81 deletions(-) diff --git a/src/qibolab/pulses/__init__.py b/src/qibolab/pulses/__init__.py index ff630e995..d8e1abcbd 100644 --- a/src/qibolab/pulses/__init__.py +++ b/src/qibolab/pulses/__init__.py @@ -1,2 +1,2 @@ from .envelope import * -from .pulse import Acquisition, Align, Delay, Pulse, PulseLike, VirtualZ +from .pulse import Acquisition, Align, Delay, Pulse, PulseLike, Readout, VirtualZ diff --git a/src/qibolab/pulses/pulse.py b/src/qibolab/pulses/pulse.py index c5b58ebce..f95ee52d5 100644 --- a/src/qibolab/pulses/pulse.py +++ b/src/qibolab/pulses/pulse.py @@ -103,7 +103,7 @@ class Acquisition(_PulseLike): """Duration in ns.""" -class _Readout(_PulseLike): +class Readout(_PulseLike): """Readout instruction. This event instructs the device to acquire samples for the event @@ -135,6 +135,6 @@ class Align(_PulseLike): PulseLike = Annotated[ - Union[Align, Pulse, Delay, VirtualZ, Acquisition, _Readout], + Union[Align, Pulse, Delay, VirtualZ, Acquisition, Readout], Field(discriminator="kind"), ] diff --git a/src/qibolab/sequence.py b/src/qibolab/sequence.py index d22ff8fb4..201b1bb70 100644 --- a/src/qibolab/sequence.py +++ b/src/qibolab/sequence.py @@ -2,20 +2,18 @@ from collections import UserList from collections.abc import Callable, Iterable -from itertools import zip_longest -from typing import Any +from typing import Any, Union from pydantic import TypeAdapter from pydantic_core import core_schema -from qibolab.pulses.pulse import Pulse, _Readout - -from .identifier import ChannelId, ChannelType -from .pulses import Acquisition, Align, Delay, PulseLike +from .identifier import ChannelId +from .pulses import Acquisition, Align, Delay, PulseLike, Readout __all__ = ["PulseSequence"] _Element = tuple[ChannelId, PulseLike] +InputOps = Union[Readout, Acquisition] _adapted_sequence = TypeAdapter(list[_Element]) @@ -149,7 +147,7 @@ def trim(self) -> "PulseSequence": return type(self)(reversed(new)) @property - def acquisitions(self) -> list[tuple[ChannelId, Acquisition]]: + def acquisitions(self) -> list[tuple[ChannelId, InputOps]]: """Return list of the readout pulses in this sequence. .. note:: @@ -159,32 +157,4 @@ def acquisitions(self) -> list[tuple[ChannelId, Acquisition]]: :attr:`ChannelType.ACQUISITION`) """ # pulse filter needed to exclude delays - return [el for el in self if isinstance(el[1], Acquisition)] - - @property - def as_readouts(self) -> list[_Element]: - new = [] - skip = False - for (ch, p), (nch, np) in zip_longest(self, self[1:], fillvalue=(None, None)): - if skip: - skip = False - continue - - # TODO: replace with pattern matching, once py3.9 will be abandoned - assert ch is not None - if ch.channel_type is ChannelType.ACQUISITION and not isinstance(p, Delay): - raise ValueError("Acquisition not preceded by probe.") - if ch.channel_type is ChannelType.PROBE and isinstance(p, Pulse): - if ( - nch is not None - and nch.channel_type is ChannelType.ACQUISITION - and isinstance(np, Acquisition) - ): - new.append((ch, _Readout(acquisition=np, probe=p))) - skip = True - else: - raise ValueError("Probe not followed by acquisition.") - else: - new.append((ch, p)) - - return new + return [(ch, p) for ch, p in self if isinstance(p, (Acquisition, Readout))] diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 6fc25df0e..e1ce15fac 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -1,17 +1,15 @@ -import pytest from pydantic import TypeAdapter -from qibolab.identifier import ChannelId from qibolab.pulses import ( Acquisition, Delay, Drag, Gaussian, Pulse, + Readout, Rectangular, VirtualZ, ) -from qibolab.pulses.pulse import _Readout from qibolab.sequence import PulseSequence @@ -21,9 +19,9 @@ def test_init(): def test_init_with_iterable(): - sc = ChannelId.load("some/probe") - oc = ChannelId.load("other/drive") - c5 = ChannelId.load("5/drive") + sc = "some/probe" + oc = "other/drive" + c5 = "5/drive" seq = PulseSequence( [ (sc, p) @@ -51,10 +49,10 @@ def test_init_with_iterable(): def test_serialization(): - sp = ChannelId.load("some/probe") - sa = ChannelId.load("some/acquisition") - od = ChannelId.load("other/drive") - of = ChannelId.load("other/flux") + sp = "some/probe" + sa = "some/acquisition" + od = "other/drive" + of = "other/flux" seq = PulseSequence( [ @@ -281,45 +279,24 @@ def test_acquisitions(): def test_readouts(): probe = Pulse(duration=10, amplitude=1, envelope=Rectangular()) acq = Acquisition(duration=10) - sequence = PulseSequence.load([("1/probe", probe), ("1/acquisition", acq)]) - ros = sequence.as_readouts - assert len(ros) == 1 - ro = ros[0][1] - assert isinstance(ro, _Readout) + sequence = PulseSequence([("1/acquisition", Readout(probe=probe, acquisition=acq))]) + assert len(sequence) == 1 + ro = sequence[0][1] + assert isinstance(ro, Readout) assert ro.duration == acq.duration assert ro.id == acq.id - sequence = PulseSequence.load( + sequence = PulseSequence( [ ("1/drive", VirtualZ(phase=0.7)), - ("1/probe", Delay(duration=15)), ("1/acquisition", Delay(duration=20)), - ("1/probe", probe), - ("1/acquisition", acq), + ("1/acquisition", Readout(probe=probe, acquisition=acq)), ("1/flux", probe), ] ) - ros = sequence.as_readouts - assert len(ros) == 5 - - sequence = PulseSequence.load([("1/probe", probe)]) - with pytest.raises(ValueError, match="(?i)probe"): - sequence.as_readouts - - sequence = PulseSequence.load([("1/acquisition", acq)]) - with pytest.raises(ValueError, match="(?i)acquisition"): - sequence.as_readouts - - sequence = PulseSequence.load([("1/acquisition", acq), ("1/probe", probe)]) - with pytest.raises(ValueError): - sequence.as_readouts + assert len(sequence) == 4 + assert len(sequence.acquisitions) == 1 + assert isinstance(sequence.acquisitions[0][1], Readout) - sequence = PulseSequence.load( - [ - ("1/probe", probe), - ("1/acquisition", Delay(duration=20)), - ("1/acquisition", acq), - ] - ) - with pytest.raises(ValueError): - sequence.as_readouts + aslist = TypeAdapter(PulseSequence).dump_python(sequence) + assert PulseSequence.load(aslist) == sequence From 890e5bdf1175788c432af937abad8cbf3b9d2df2 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 16:02:02 +0200 Subject: [PATCH 19/79] test: Drop channel id test --- tests/pulses/test_pulse.py | 4 ++-- tests/test_identifier.py | 25 +------------------------ 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/tests/pulses/test_pulse.py b/tests/pulses/test_pulse.py index c0b35e7d7..d88c85549 100644 --- a/tests/pulses/test_pulse.py +++ b/tests/pulses/test_pulse.py @@ -4,7 +4,7 @@ from pytest import approx, raises from qibolab.pulses import Acquisition, Custom, Pulse, Rectangular, VirtualZ -from qibolab.pulses.pulse import _Readout +from qibolab.pulses.pulse import Readout def test_virtual_z(): @@ -15,7 +15,7 @@ def test_virtual_z(): def test_readout(): p = Pulse(duration=5, amplitude=0.9, envelope=Rectangular()) a = Acquisition(duration=60) - r = _Readout(acquisition=a, probe=p) + r = Readout(acquisition=a, probe=p) assert r.duration == a.duration assert r.id == a.id diff --git a/tests/test_identifier.py b/tests/test_identifier.py index e6d627392..86dab3340 100644 --- a/tests/test_identifier.py +++ b/tests/test_identifier.py @@ -1,28 +1,5 @@ -import pytest -from pydantic import ValidationError - -from qibolab.identifier import ChannelId, ChannelType +from qibolab.identifier import ChannelType def test_channel_type(): assert str(ChannelType.ACQUISITION) == "acquisition" - - -def test_channel_id(): - name = "1/probe" - ch = ChannelId.load(name) - assert ch.qubit == 1 - assert ch.channel_type is ChannelType.PROBE - assert ch.cross is None - assert str(ch) == name == ch.model_dump() - - chd = ChannelId.load("10/drive_cross/3") - assert chd.qubit == 10 - assert chd.channel_type is ChannelType.DRIVE_CROSS - assert chd.cross == "3" - - with pytest.raises(ValidationError): - ChannelId.load("1/probe/3") - - with pytest.raises(ValueError): - ChannelId.load("ciao/come/va/bene") From 3cb1fbb32d9ce3f2ff0751d4ed69cd9d0d5e6b32 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 16:14:10 +0200 Subject: [PATCH 20/79] feat: Move actual channels to instrument (just dummy) --- src/qibolab/dummy/platform.py | 32 ++++++++++++++++++++++---------- src/qibolab/instruments/dummy.py | 5 ++++- tests/conftest.py | 5 ++--- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index d3d12bc23..0a40837a1 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -17,21 +17,33 @@ def create_dummy() -> Platform: qubits = {} # attach the channels for q in range(5): - probe, acquisition = f"qubit_{q}/probe", f"qubit_{q}/acquisition" + drive, drive12, flux, probe, acquisition = ( + f"qubit_{q}/drive", + f"qubit_{q}/drive12", + f"qubit_{q}/flux", + f"qubit_{q}/probe", + f"qubit_{q}/acquisition", + ) + instrument.channels |= { + probe: IqChannel(mixer=None, lo=None, acquisition=acquisition), + acquisition: AcquireChannel(twpa_pump=pump.name, probe=probe), + drive: IqChannel(mixer=None, lo=None), + drive12: IqChannel(mixer=None, lo=None), + flux: DcChannel(), + } qubits[q] = Qubit( - name=q, - probe=IqChannel(name=probe, mixer=None, lo=None, acquisition=acquisition), - acquisition=AcquireChannel( - name=acquisition, twpa_pump=pump.name, probe=probe - ), - drive=IqChannel(name=f"qubit_{q}/drive", mixer=None, lo=None), - drive12=IqChannel(name=f"qubit_{q}/drive12", mixer=None, lo=None), - flux=DcChannel(name=f"qubit_{q}/flux"), + probe=probe, + acquisition=acquisition, + drive=drive, + drive_qudits={"1-2": f"qubit_{q}/flux"}, + flux=flux, ) couplers = {} for c in (0, 1, 3, 4): - couplers[c] = Qubit(name=c, flux=DcChannel(name=f"coupler_{c}/flux")) + flux = f"coupler_{c}/flux" + instrument.channels |= {flux: DcChannel()} + couplers[c] = Qubit(flux=flux) return Platform.load( path=FOLDER, instruments=[instrument, pump], qubits=qubits, couplers=couplers diff --git a/src/qibolab/instruments/dummy.py b/src/qibolab/instruments/dummy.py index 36c678f23..5c5a215b8 100644 --- a/src/qibolab/instruments/dummy.py +++ b/src/qibolab/instruments/dummy.py @@ -1,9 +1,11 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field import numpy as np from qibo.config import log from qibolab import AcquisitionType, AveragingMode, ExecutionParameters +from qibolab.components.channels import Channel +from qibolab.identifier import ChannelId from qibolab.pulses.pulse import Acquisition from qibolab.sequence import PulseSequence from qibolab.sweeper import ParallelSweepers @@ -67,6 +69,7 @@ class DummyInstrument(Controller): name: str address: str bounds: str = "dummy/bounds" + channels: dict[ChannelId, Channel] = field(default_factory=dict) @property def sampling_rate(self) -> int: diff --git a/tests/conftest.py b/tests/conftest.py index d79f28794..0d443be63 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -154,15 +154,14 @@ def wrapped( sequence.concatenate(qd_seq) sequence.concatenate(probe_seq) if sweepers is None: - amp_values = np.arange(0, 0.8, 0.1) sweeper1 = Sweeper( parameter=Parameter.offset, range=(0.01, 0.06, 0.01), - channels=[qubit.flux.name], + channels=[qubit.flux], ) sweeper2 = Sweeper( parameter=Parameter.amplitude, - values=amp_values, + range=(0, 0.8, 0.1), pulses=[probe_pulse], ) sweepers = [[sweeper1], [sweeper2]] From 68d60b173f2a2c89529b2bf2f323bd22cac28fb0 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 16:39:43 +0200 Subject: [PATCH 21/79] fix: Migrate even instruments to Pydantic --- src/qibolab/instruments/abstract.py | 35 +++++++++++++---------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/qibolab/instruments/abstract.py b/src/qibolab/instruments/abstract.py index 10c7f0db2..46ed02ffd 100644 --- a/src/qibolab/instruments/abstract.py +++ b/src/qibolab/instruments/abstract.py @@ -1,30 +1,27 @@ from abc import ABC, abstractmethod -from dataclasses import asdict, dataclass from typing import Optional import numpy.typing as npt +from pydantic import ConfigDict, Field from qibolab.components import Config +from qibolab.components.channels import Channel from qibolab.execution_parameters import ExecutionParameters +from qibolab.identifier import ChannelId from qibolab.sequence import PulseSequence +from qibolab.serialize import Model from qibolab.sweeper import ParallelSweepers InstrumentId = str -@dataclass -class InstrumentSettings: +class InstrumentSettings(Model): """Container of settings that are dumped in the platform runcard json.""" - def dump(self): - """Dictionary containing the settings. - - Useful when dumping the instruments to the runcard JSON. - """ - return asdict(self) + model_config = ConfigDict(frozen=False) -class Instrument(ABC): +class Instrument(Model, ABC): """Parent class for all the instruments connected via TCPIP. Args: @@ -32,11 +29,12 @@ class Instrument(ABC): address (str): Instrument network address. """ - def __init__(self, name, address): - self.name: InstrumentId = name - self.address: str = address - self.is_connected: bool = False - self.settings: Optional[InstrumentSettings] = None + model_config = ConfigDict(frozen=False) + + name: InstrumentId + address: str + is_connected: bool = False + settings: Optional[InstrumentSettings] = None @property def signature(self): @@ -58,10 +56,9 @@ def setup(self, *args, **kwargs): class Controller(Instrument): """Instrument that can play pulses (using waveform generator).""" - def __init__(self, name, address): - super().__init__(name, address) - self.bounds: str - """Estimated limitations of the device memory.""" + bounds: str + """Estimated limitations of the device memory.""" + channels: dict[ChannelId, Channel] = Field(default_factory=dict) @property @abstractmethod From 8edc9772e3d58d2323834b5b5b14a9f8bb322a80 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 16:40:44 +0200 Subject: [PATCH 22/79] fix: Migrate the oscillator base classes --- src/qibolab/instruments/oscillator.py | 56 +++++++++++---------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/src/qibolab/instruments/oscillator.py b/src/qibolab/instruments/oscillator.py index 3fb7b4123..75c732af3 100644 --- a/src/qibolab/instruments/oscillator.py +++ b/src/qibolab/instruments/oscillator.py @@ -1,14 +1,15 @@ from abc import abstractmethod -from dataclasses import dataclass, fields from typing import Optional +from pydantic import Field +from qcodes.instrument import Instrument as QcodesInstrument + from qibolab.instruments.abstract import Instrument, InstrumentSettings RECONNECTION_ATTEMPTS = 3 """Number of times to attempt connecting to instrument in case of failure.""" -@dataclass class LocalOscillatorSettings(InstrumentSettings): """Local oscillator parameters that are saved in the platform runcard.""" @@ -16,17 +17,6 @@ class LocalOscillatorSettings(InstrumentSettings): frequency: Optional[float] = None ref_osc_source: Optional[str] = None - def dump(self): - """Dictionary containing local oscillator settings. - - The reference clock is excluded as it is not a calibrated - parameter. None values are also excluded. - """ - data = super().dump() - return { - k: v for k, v in data.items() if k != "ref_osc_source" and v is not None - } - def _setter(instrument, parameter, value): """Set value of a setting. @@ -44,10 +34,11 @@ def _setter(instrument, parameter, value): def _property(parameter): - """Creates an instrument property.""" - getter = lambda self: getattr(self.settings, parameter) - setter = lambda self, value: _setter(self, parameter, value) - return property(getter, setter) + """Create an instrument property.""" + return property( + lambda self: getattr(self.settings, parameter), + lambda self, value: _setter(self, parameter, value), + ) class LocalOscillator(Instrument): @@ -58,22 +49,21 @@ class LocalOscillator(Instrument): qubits and resonators. They cannot be used to play or sweep pulses. """ + device: Optional[QcodesInstrument] = None + settings: Optional[InstrumentSettings] = Field( + default_factory=lambda: LocalOscillatorSettings() + ) + frequency = _property("frequency") power = _property("power") ref_osc_source = _property("ref_osc_source") - def __init__(self, name, address, ref_osc_source=None): - super().__init__(name, address) - self.device = None - self.settings = LocalOscillatorSettings(ref_osc_source=ref_osc_source) - @abstractmethod - def create(self): + def create(self) -> QcodesInstrument: """Create instance of physical device.""" def connect(self): - """Connects to the instrument using the IP address set in the - runcard.""" + """Connect to the instrument.""" if not self.is_connected: self.device = self.create() self.is_connected = True @@ -84,13 +74,15 @@ def connect(self): f"There is an open connection to the instrument {self.name}." ) - for fld in fields(self.settings): - self.sync(fld.name) + assert self.settings is not None + for fld in self.settings.model_fields: + self.sync(fld) self.device.on() def disconnect(self): if self.is_connected: + assert self.device is not None self.device.off() self.device.close() self.is_connected = False @@ -105,6 +97,7 @@ def sync(self, parameter): parameter (str): Parameter name to be synced. """ value = getattr(self, parameter) + assert self.device is not None if value is None: setattr(self.settings, parameter, self.device.get(parameter)) else: @@ -119,11 +112,8 @@ def setup(self, **kwargs): Args: **kwargs: Instrument settings loaded from the runcard. """ - type_ = self.__class__ - _fields = {fld.name for fld in fields(self.settings)} + assert self.settings is not None for name, value in kwargs.items(): - if name not in _fields: - raise KeyError( - f"Cannot set {name} to instrument {self.name} of type {type_.__name__}" - ) + if name not in self.settings.model_fields: + raise KeyError(f"Cannot set {name} to instrument {self.name}") setattr(self, name, value) From c8619addef39ccab18f7148095a26988cb6c1fca Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 16:41:09 +0200 Subject: [PATCH 23/79] fix: Migrate dummy as well --- src/qibolab/dummy/platform.py | 4 ++-- src/qibolab/instruments/dummy.py | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index 0a40837a1..b4286a840 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -11,8 +11,8 @@ def create_dummy() -> Platform: """Create a dummy platform using the dummy instrument.""" # register the instruments - instrument = DummyInstrument("dummy", "0.0.0.0") - pump = DummyLocalOscillator("twpa_pump", "0.0.0.0") + instrument = DummyInstrument(name="dummy", address="0.0.0.0") + pump = DummyLocalOscillator(name="twpa_pump", address="0.0.0.0") qubits = {} # attach the channels diff --git a/src/qibolab/instruments/dummy.py b/src/qibolab/instruments/dummy.py index 5c5a215b8..2b16b65e8 100644 --- a/src/qibolab/instruments/dummy.py +++ b/src/qibolab/instruments/dummy.py @@ -1,11 +1,7 @@ -from dataclasses import dataclass, field - import numpy as np from qibo.config import log from qibolab import AcquisitionType, AveragingMode, ExecutionParameters -from qibolab.components.channels import Channel -from qibolab.identifier import ChannelId from qibolab.pulses.pulse import Acquisition from qibolab.sequence import PulseSequence from qibolab.sweeper import ParallelSweepers @@ -52,7 +48,6 @@ def create(self): return DummyDevice() -@dataclass class DummyInstrument(Controller): """Dummy instrument that returns random voltage values. @@ -69,7 +64,6 @@ class DummyInstrument(Controller): name: str address: str bounds: str = "dummy/bounds" - channels: dict[ChannelId, Channel] = field(default_factory=dict) @property def sampling_rate(self) -> int: From f9d54d3146a5bf51293b369384f2488af00f07cb Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 16:45:43 +0200 Subject: [PATCH 24/79] test: Readapt platform tests to unstructured channel ids --- tests/test_platform.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/test_platform.py b/tests/test_platform.py index 4367d2ce5..c300796e6 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -30,6 +30,22 @@ nshots = 1024 +def test_unroll_sequences(platform: Platform): + qubit = next(iter(platform.qubits.values())) + assert qubit.probe is not None + natives = platform.natives.single_qubit[0] + assert natives.RX is not None + assert natives.MZ is not None + sequence = PulseSequence() + sequence.concatenate(natives.RX.create_sequence()) + sequence.append((qubit.probe, Delay(duration=sequence.duration))) + sequence.concatenate(natives.MZ.create_sequence()) + total_sequence, readouts = unroll_sequences(10 * [sequence], relaxation_time=10000) + assert len(total_sequence.acquisitions) == 10 + assert len(readouts) == 1 + assert all(len(readouts[acq.id]) == 10 for _, acq in sequence.acquisitions) + + def test_create_platform(platform): assert isinstance(platform, Platform) @@ -151,17 +167,17 @@ def test_dump_parameters(platform: Platform, tmp_path: Path): def test_dump_parameters_with_updates(platform: Platform, tmp_path: Path): qubit = next(iter(platform.qubits.values())) - frequency = platform.config(str(qubit.drive.name)).frequency + 1.5e9 - smearing = platform.config(str(qubit.acquisition.name)).smearing + 10 + frequency = platform.config(qubit.drive).frequency + 1.5e9 + smearing = platform.config(qubit.acquisition).smearing + 10 update = { - str(qubit.drive.name): {"frequency": frequency}, - str(qubit.acquisition.name): {"smearing": smearing}, + str(qubit.drive): {"frequency": frequency}, + str(qubit.acquisition): {"smearing": smearing}, } update_configs(platform.parameters.configs, [update]) (tmp_path / PARAMETERS).write_text(platform.parameters.model_dump_json()) final = Parameters.model_validate_json((tmp_path / PARAMETERS).read_text()) - assert final.configs[str(qubit.drive.name)].frequency == frequency - assert final.configs[str(qubit.acquisition.name)].smearing == smearing + assert final.configs[qubit.drive].frequency == frequency + assert final.configs[qubit.acquisition].smearing == smearing def test_kernels(tmp_path: Path): @@ -183,8 +199,8 @@ def test_kernels(tmp_path: Path): ) for qubit in platform.qubits.values(): - orig = platform.parameters.configs[str(qubit.acquisition.name)].kernel - load = reloaded.parameters.configs[str(qubit.acquisition.name)].kernel + orig = platform.parameters.configs[qubit.acquisition].kernel + load = reloaded.parameters.configs[qubit.acquisition].kernel np.testing.assert_array_equal(orig, load) From d133ed97f2b7bb68a86042527aea02a4bad5ad58 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 16:49:16 +0200 Subject: [PATCH 25/79] test: Redapt native tests to unstructured channel ids --- tests/test_native.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tests/test_native.py b/tests/test_native.py index 37cae537c..460dad149 100644 --- a/tests/test_native.py +++ b/tests/test_native.py @@ -4,7 +4,6 @@ import pytest from pydantic import TypeAdapter -from qibolab.identifier import ChannelId from qibolab.native import FixedSequenceFactory, RxyFactory, TwoQubitNatives from qibolab.pulses import ( Drag, @@ -18,7 +17,7 @@ def test_fixed_sequence_factory(): - seq = PulseSequence.load( + seq = PulseSequence( [ ( "channel_1/probe", @@ -37,7 +36,7 @@ def test_fixed_sequence_factory(): assert fseq1 == seq assert fseq2 == seq - np = ChannelId.load("new/probe") + np = "new/probe" fseq1.append( ( np, @@ -62,7 +61,7 @@ def test_fixed_sequence_factory(): ], ) def test_rxy_rotation_factory(args, amplitude, phase): - seq = PulseSequence.load( + seq = PulseSequence( [ ( "1/drive", @@ -75,17 +74,17 @@ def test_rxy_rotation_factory(args, amplitude, phase): fseq1 = factory.create_sequence(**args) fseq2 = factory.create_sequence(**args) assert fseq1 == fseq2 - np = ChannelId.load("new/probe") + np = "new/probe" fseq2.append((np, Pulse(duration=56, amplitude=0.43, envelope=Rectangular()))) assert np not in fseq1.channels - pulse = next(iter(fseq1.channel(ChannelId.load("1/drive")))) + pulse = next(iter(fseq1.channel("1/drive"))) assert pulse.amplitude == pytest.approx(amplitude) assert pulse.relative_phase == pytest.approx(phase) def test_rxy_factory_multiple_channels(): - seq = PulseSequence.load( + seq = PulseSequence( [ ( "1/drive", @@ -103,7 +102,7 @@ def test_rxy_factory_multiple_channels(): def test_rxy_factory_multiple_pulses(): - seq = PulseSequence.load( + seq = PulseSequence( [ ( "1/drive", @@ -131,13 +130,8 @@ def test_rxy_factory_multiple_pulses(): ], ) def test_rxy_rotation_factory_envelopes(envelope): - seq = PulseSequence.load( - [ - ( - "1/drive", - Pulse(duration=100, amplitude=1.0, envelope=envelope), - ) - ] + seq = PulseSequence( + [("1/drive", Pulse(duration=100, amplitude=1.0, envelope=envelope))] ) if isinstance(envelope, (Gaussian, Drag)): From 7bf242b91642685b438ab911c07d2ce298aace18 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 17:44:40 +0200 Subject: [PATCH 26/79] feat: Extend identifiers to states --- src/qibolab/identifier.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/qibolab/identifier.py b/src/qibolab/identifier.py index 6a077aa4d..4db4a3522 100644 --- a/src/qibolab/identifier.py +++ b/src/qibolab/identifier.py @@ -4,7 +4,7 @@ from pydantic import BeforeValidator, Field, PlainSerializer QubitId = Annotated[Union[int, str], Field(union_mode="left_to_right")] -"""Type for qubit names.""" +"""Qubit name.""" QubitPairId = Annotated[ tuple[QubitId, QubitId], @@ -33,3 +33,21 @@ def __str__(self) -> str: ChannelId = str """Unique identifier for a channel.""" + + +StateId = int +"""State identifier.""" + +TransitionId = Annotated[ + tuple[StateId, StateId], + BeforeValidator(lambda p: tuple(p.split("-")) if isinstance(p, str) else p), + PlainSerializer(lambda p: f"{p[0]}-{p[1]}"), +] +"""Identifier for a state transition.""" + +QubitPairId = Annotated[ + tuple[QubitId, QubitId], + BeforeValidator(lambda p: tuple(p.split("-")) if isinstance(p, str) else p), + PlainSerializer(lambda p: f"{p[0]}-{p[1]}"), +] +"""Two-qubit active interaction identifier.""" From 0a002c7961039e2f7f3af0c3c335ab22360cc4b5 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 17:45:07 +0200 Subject: [PATCH 27/79] feat: Expose channels from qubits and platform --- src/qibolab/platform/platform.py | 12 ++++++-- src/qibolab/qubits.py | 47 ++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/qibolab/platform/platform.py b/src/qibolab/platform/platform.py index de6942b3b..67e04181a 100644 --- a/src/qibolab/platform/platform.py +++ b/src/qibolab/platform/platform.py @@ -10,6 +10,7 @@ from qibo.config import log, raise_error from qibolab.components import Config +from qibolab.components.channels import Channel from qibolab.execution_parameters import ExecutionParameters from qibolab.identifier import ChannelId, QubitId, QubitPairId from qibolab.instruments.abstract import Controller, Instrument, InstrumentId @@ -55,7 +56,7 @@ def estimate_duration( def _channels_map(elements: QubitMap) -> dict[ChannelId, QubitId]: """Map channel names to element (qubit or coupler).""" - return {ch.name: id for id, el in elements.items() for ch in el.channels} + return {ch: id for id, el in elements.items() for ch in el.channels} @dataclass @@ -133,9 +134,14 @@ def components(self) -> set[str]: return set(self.parameters.configs.keys()) @property - def channels(self) -> list[ChannelId]: + def channels(self) -> dict[ChannelId, Channel]: """Channels in the platform.""" - return list(self.qubit_channels) + list(self.coupler_channels) + return { + id: ch + for instr in self.instruments.values() + if isinstance(instr, Controller) + for id, ch in instr.channels.items() + } @property def qubit_channels(self) -> dict[ChannelId, QubitId]: diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 232fac002..fc12367e7 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -1,10 +1,8 @@ -from collections.abc import Iterable from typing import Optional -from pydantic import ConfigDict +from pydantic import ConfigDict, Field -from .components.channels import Channel -from .identifier import ChannelId, ChannelType, QubitId +from .identifier import ChannelId, TransitionId from .serialize import Model @@ -13,28 +11,35 @@ class Qubit(Model): Qubit objects are instantiated by :class:`qibolab.platforms.platform.Platform` but they are passed to instrument designs in order to play pulses. - - Args: - name (int, str): Qubit number or name. - readout (:class:`qibolab.platforms.utils.Channel`): Channel used to - readout pulses to the qubit. - drive (:class:`qibolab.platforms.utils.Channel`): Channel used to - send drive pulses to the qubit. - flux (:class:`qibolab.platforms.utils.Channel`): Channel used to - send flux pulses to the qubit. """ model_config = ConfigDict(frozen=False) - probe: Optional[ChannelId] = None - acquisition: Optional[ChannelId] = None drive: Optional[ChannelId] = None - drive_qudits: Optional[dict[str, ChannelId]] = None + """Ouput channel, to drive the qubit state.""" + drive_qudits: dict[TransitionId, ChannelId] = Field(default_factory=dict) + """Output channels collection, to drive non-qubit transitions.""" flux: Optional[ChannelId] = None + """Output channel, to control the qubit flux.""" + probe: Optional[ChannelId] = None + """Output channel, to probe the resonator.""" + acquisition: Optional[ChannelId] = None + """Input channel, to acquire the readout results.""" @property - def channels(self) -> Iterable[Channel]: - for ct in ChannelType: - channel = getattr(self, ct.value) - if channel is not None: - yield channel + def channels(self) -> list[ChannelId]: + return [ + x + for x in ( + [getattr(self, ch) for ch in ["probe", "acquisition", "drive", "flux"]] + + list(self.drive_qudits.values()) + ) + if x is not None + ] + + +class QubitPair(Model): + """Represent a two-qubit interaction.""" + + drive: Optional[ChannelId] = None + """Output channel, for cross-resonance driving.""" From 0fe93b657a258846ca9e4c45fdf5903f61e9ec3c Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 17:45:34 +0200 Subject: [PATCH 28/79] fix: Fix identifiers imports --- src/qibolab/compilers/compiler.py | 3 +-- src/qibolab/dummy/platform.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/qibolab/compilers/compiler.py b/src/qibolab/compilers/compiler.py index a5ae62085..fc5b79741 100644 --- a/src/qibolab/compilers/compiler.py +++ b/src/qibolab/compilers/compiler.py @@ -15,10 +15,9 @@ rz_rule, z_rule, ) -from qibolab.identifier import ChannelId +from qibolab.identifier import ChannelId, QubitId from qibolab.platform import Platform from qibolab.pulses import Delay -from qibolab.qubits import QubitId from qibolab.sequence import PulseSequence Rule = Callable[..., PulseSequence] diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index b4286a840..d45333839 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -35,7 +35,7 @@ def create_dummy() -> Platform: probe=probe, acquisition=acquisition, drive=drive, - drive_qudits={"1-2": f"qubit_{q}/flux"}, + drive_qudits={(1, 2): f"qubit_{q}/flux"}, flux=flux, ) From c3737fdfa9eb68d6b6f5e35aa9e82aa307310442 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 18:04:53 +0200 Subject: [PATCH 29/79] feat: Change qubit retrieval from platform To also expose translated names, not present any longer in the qubit object --- src/qibolab/compilers/compiler.py | 26 ++++++++++++-------------- src/qibolab/compilers/default.py | 6 +++--- src/qibolab/platform/platform.py | 28 ++++++++++++++-------------- tests/test_backends.py | 16 ++++++++-------- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/qibolab/compilers/compiler.py b/src/qibolab/compilers/compiler.py index fc5b79741..20b3ddd15 100644 --- a/src/qibolab/compilers/compiler.py +++ b/src/qibolab/compilers/compiler.py @@ -96,25 +96,23 @@ def get_sequence(self, gate: gates.Gate, platform: Platform) -> PulseSequence: natives = platform.natives if isinstance(gate, (gates.M)): - qubits = [ - natives.single_qubit[platform.get_qubit(q).name] for q in gate.qubits - ] + qubits = [natives.single_qubit[platform.qubit(q)[0]] for q in gate.qubits] return rule(gate, qubits) if isinstance(gate, (gates.Align)): - qubits = [platform.get_qubit(q) for q in gate.qubits] + qubits = [platform.qubit(q)[1] for q in gate.qubits] return rule(gate, qubits) if isinstance(gate, (gates.Z, gates.RZ)): - qubit = platform.get_qubit(gate.target_qubits[0]) + qubit = platform.qubit(gate.target_qubits[0])[1] return rule(gate, qubit) if len(gate.qubits) == 1: - qubit = platform.get_qubit(gate.target_qubits[0]) - return rule(gate, natives.single_qubit[qubit.name]) + qubit = platform.qubit(gate.target_qubits[0])[0] + return rule(gate, natives.single_qubit[qubit]) if len(gate.qubits) == 2: - pair = tuple(platform.get_qubit(q).name for q in gate.qubits) + pair = tuple(platform.qubit(q)[0] for q in gate.qubits) assert len(pair) == 2 return rule(gate, natives.two_qubit[pair]) @@ -127,10 +125,10 @@ def _compile_gate( channel_clock: defaultdict[ChannelId, float], ) -> PulseSequence: def qubit_clock(el: QubitId): - return max(channel_clock[ch.name] for ch in platform.qubits[el].channels) + return max(channel_clock[ch] for ch in platform.qubits[el].channels) def coupler_clock(el: QubitId): - return max(channel_clock[ch.name] for ch in platform.couplers[el].channels) + return max(channel_clock[ch] for ch in platform.couplers[el].channels) gate_seq = self.get_sequence(gate, platform) # qubits receiving pulses @@ -163,14 +161,14 @@ def coupler_clock(el: QubitId): end = start + gate_seq.duration final = PulseSequence() for q in gate.qubits: - qubit = platform.get_qubit(q) + qubit = platform.qubit(q)[1] # all actual qubits have a non-null drive channel, and couplers are not # explicitedly listed in gates assert qubit.drive is not None - delay = end - channel_clock[qubit.drive.name] + delay = end - channel_clock[qubit.drive] if delay > 0: - final.append((qubit.drive.name, Delay(duration=delay))) - channel_clock[qubit.drive.name] += delay + final.append((qubit.drive, Delay(duration=delay))) + channel_clock[qubit.drive] += delay # couplers do not require individual padding, because they do are only # involved in gates where both of the other qubits are involved diff --git a/src/qibolab/compilers/default.py b/src/qibolab/compilers/default.py index 89824cfa4..d9e5aaa5e 100644 --- a/src/qibolab/compilers/default.py +++ b/src/qibolab/compilers/default.py @@ -16,12 +16,12 @@ def z_rule(gate: Gate, qubit: Qubit) -> PulseSequence: """Z gate applied virtually.""" - return PulseSequence([(qubit.drive.name, VirtualZ(phase=math.pi))]) + return PulseSequence([(qubit.drive, VirtualZ(phase=math.pi))]) def rz_rule(gate: Gate, qubit: Qubit) -> PulseSequence: """RZ gate applied virtually.""" - return PulseSequence([(qubit.drive.name, VirtualZ(phase=gate.parameters[0]))]) + return PulseSequence([(qubit.drive, VirtualZ(phase=gate.parameters[0]))]) def identity_rule(gate: Gate, natives: SingleQubitNatives) -> PulseSequence: @@ -71,5 +71,5 @@ def align_rule(gate: Align, qubits: list[Qubit]) -> PulseSequence: if delay == 0.0: return PulseSequence() return PulseSequence( - [(ch.name, Delay(duration=delay)) for qubit in qubits for ch in qubit.channels] + [(ch, Delay(duration=delay)) for qubit in qubits for ch in qubit.channels] ) diff --git a/src/qibolab/platform/platform.py b/src/qibolab/platform/platform.py index 67e04181a..173ce708a 100644 --- a/src/qibolab/platform/platform.py +++ b/src/qibolab/platform/platform.py @@ -308,26 +308,26 @@ def dump(self, path: Path): """Dump platform.""" (path / PARAMETERS).write_text(self.parameters.model_dump_json(indent=4)) - def get_qubit(self, qubit: QubitId) -> Qubit: - """Return the name of the physical qubit corresponding to a logical - qubit. + def _element(self, qubit: QubitId, coupler=False) -> tuple[QubitId, Qubit]: + elements = self.qubits if not coupler else self.couplers + try: + return qubit, elements[qubit] + except KeyError: + assert isinstance(qubit, int) + return list(self.qubits.items())[qubit] + + def qubit(self, qubit: QubitId) -> tuple[QubitId, Qubit]: + """Retrieve physical qubit name and object. Temporary fix for the compiler to work for platforms where the qubits are not named as 0, 1, 2, ... """ - try: - return self.qubits[qubit] - except KeyError: - return list(self.qubits.values())[qubit] + return self._element(qubit) - def get_coupler(self, coupler: QubitId) -> Qubit: - """Return the name of the physical coupler corresponding to a logical - coupler. + def coupler(self, coupler: QubitId) -> tuple[QubitId, Qubit]: + """Retrieve physical coupler name and object. Temporary fix for the compiler to work for platforms where the couplers are not named as 0, 1, 2, ... """ - try: - return self.couplers[coupler] - except KeyError: - return list(self.couplers.values())[coupler] + return self._element(coupler, coupler=True) diff --git a/tests/test_backends.py b/tests/test_backends.py index 48e82905d..fde4ff418 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -9,6 +9,7 @@ from qibolab import MetaBackend, create_platform from qibolab.backends import QibolabBackend +from qibolab.platform.platform import Platform def generate_circuit_with_gate(nqubits, gate, **kwargs): @@ -108,7 +109,6 @@ def dummy_string_qubit_names(): platform = create_platform("dummy") for q, qubit in platform.qubits.copy().items(): name = f"A{q}" - qubit.name = name platform.qubits[name] = qubit del platform.qubits[q] platform.natives.single_qubit[name] = platform.natives.single_qubit[q] @@ -141,7 +141,7 @@ def test_execute_circuit_str_qubit_names(): @pytest.mark.xfail( raises=AssertionError, reason="Probabilities are not well calibrated" ) -def test_ground_state_probabilities_circuit(connected_backend): +def test_ground_state_probabilities_circuit(connected_backend: QibolabBackend): nshots = 5000 nqubits = connected_backend.platform.nqubits circuit = Circuit(nqubits) @@ -159,7 +159,7 @@ def test_ground_state_probabilities_circuit(connected_backend): @pytest.mark.xfail( raises=AssertionError, reason="Probabilities are not well calibrated" ) -def test_excited_state_probabilities_circuit(connected_backend): +def test_excited_state_probabilities_circuit(connected_backend: QibolabBackend): nshots = 5000 nqubits = connected_backend.platform.nqubits circuit = Circuit(nqubits) @@ -178,7 +178,7 @@ def test_excited_state_probabilities_circuit(connected_backend): @pytest.mark.xfail( raises=AssertionError, reason="Probabilities are not well calibrated" ) -def test_superposition_for_all_qubits(connected_backend): +def test_superposition_for_all_qubits(connected_backend: QibolabBackend): """Applies an H gate to each qubit of the circuit and measures the probabilities.""" nshots = 5000 @@ -205,20 +205,20 @@ def test_superposition_for_all_qubits(connected_backend): # TODO: test_circuit_result_representation -def test_metabackend_load(platform): +def test_metabackend_load(platform: Platform): backend = MetaBackend.load(platform.name) assert isinstance(backend, QibolabBackend) assert backend.platform.name == platform.name -def test_metabackend_list_available(tmpdir): +def test_metabackend_list_available(tmp_path: Path): for platform in ( "valid_platform/platform.py", "invalid_platform/invalid_platform.py", ): - path = Path(tmpdir / platform) + path = tmp_path / platform path.parent.mkdir(parents=True, exist_ok=True) path.touch() - os.environ["QIBOLAB_PLATFORMS"] = str(tmpdir) + os.environ["QIBOLAB_PLATFORMS"] = str(tmp_path) available_platforms = {"valid_platform": True} assert MetaBackend().list_available() == available_platforms From 4b6d38822015d2a6f314ae0fc75e5902fc5d108d Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 18:23:09 +0200 Subject: [PATCH 30/79] fix: Fix leftover in dummy platform definition --- src/qibolab/dummy/platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index d45333839..068ff4dc6 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -35,7 +35,7 @@ def create_dummy() -> Platform: probe=probe, acquisition=acquisition, drive=drive, - drive_qudits={(1, 2): f"qubit_{q}/flux"}, + drive_qudits={(1, 2): drive12}, flux=flux, ) From aa3279d2fe6e8f8c584f1a2fcfb9c6642d06419d Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 18:24:30 +0200 Subject: [PATCH 31/79] test: Readapt compilation tests to new qubits and channel ids --- tests/test_compilers_default.py | 57 +++++++++++++++------------------ 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/tests/test_compilers_default.py b/tests/test_compilers_default.py index 4ba4e2b82..f65e95129 100644 --- a/tests/test_compilers_default.py +++ b/tests/test_compilers_default.py @@ -6,16 +6,15 @@ from qibolab import create_platform from qibolab.compilers import Compiler -from qibolab.identifier import ChannelId, ChannelType +from qibolab.identifier import ChannelId from qibolab.native import FixedSequenceFactory, TwoQubitNatives from qibolab.platform import Platform -from qibolab.pulses import Delay +from qibolab.pulses import Delay, Pulse from qibolab.pulses.envelope import Rectangular -from qibolab.pulses.pulse import Pulse from qibolab.sequence import PulseSequence -def generate_circuit_with_gate(nqubits, gate, *params, **kwargs): +def generate_circuit_with_gate(nqubits: int, gate, *params, **kwargs): circuit = Circuit(nqubits) circuit.add(gate(q, *params, **kwargs) for q in range(nqubits)) circuit.add(gates.M(*range(nqubits))) @@ -35,7 +34,7 @@ def test_u3_sim_agreement(): np.testing.assert_allclose(u3_matrix, target_matrix) -def compile_circuit(circuit, platform) -> PulseSequence: +def compile_circuit(circuit: Circuit, platform: Platform) -> PulseSequence: """Compile a circuit to a pulse sequence.""" compiler = Compiler.default() return compiler.compile(circuit, platform)[0] @@ -51,14 +50,14 @@ def compile_circuit(circuit, platform) -> PulseSequence: (gates.RZ, np.pi / 4), ], ) -def test_compile(platform, gateargs): +def test_compile(platform: Platform, gateargs): nqubits = platform.nqubits circuit = generate_circuit_with_gate(nqubits, *gateargs) sequence = compile_circuit(circuit, platform) assert len(sequence.channels) == nqubits * int(gateargs[0] != gates.I) + nqubits * 2 -def test_compile_two_gates(platform): +def test_compile_two_gates(platform: Platform): circuit = Circuit(1) circuit.add(gates.GPI2(0, phi=0.1)) circuit.add(gates.GPI(0, 0.2)) @@ -68,11 +67,11 @@ def test_compile_two_gates(platform): qubit = platform.qubits[0] assert len(sequence.channels) == 3 - assert len(list(sequence.channel(qubit.drive.name))) == 2 - assert len(list(sequence.channel(qubit.probe.name))) == 2 # includes delay + assert len(list(sequence.channel(qubit.drive))) == 2 + assert len(list(sequence.channel(qubit.probe))) == 2 # includes delay -def test_measurement(platform): +def test_measurement(platform: Platform): nqubits = platform.nqubits circuit = Circuit(nqubits) qubits = [qubit for qubit in range(nqubits)] @@ -83,7 +82,7 @@ def test_measurement(platform): assert len(sequence.acquisitions) == 1 * nqubits -def test_rz_to_sequence(platform): +def test_rz_to_sequence(platform: Platform): circuit = Circuit(1) circuit.add(gates.RZ(0, theta=0.2)) circuit.add(gates.Z(0)) @@ -105,7 +104,7 @@ def test_gpi_to_sequence(platform: Platform): np.testing.assert_allclose(sequence.duration, rx_seq.duration) -def test_gpi2_to_sequence(platform): +def test_gpi2_to_sequence(platform: Platform): natives = platform.natives circuit = Circuit(1) @@ -154,14 +153,14 @@ def test_add_measurement_to_sequence(platform: Platform): sequence = compile_circuit(circuit, platform) qubit = platform.qubits[0] assert len(sequence.channels) == 3 - assert len(list(sequence.channel(qubit.drive.name))) == 2 - assert len(list(sequence.channel(qubit.probe.name))) == 2 # include delay + assert len(list(sequence.channel(qubit.drive))) == 2 + assert len(list(sequence.channel(qubit.probe))) == 2 # include delay s = PulseSequence() s.concatenate(natives.single_qubit[0].RX.create_sequence(theta=np.pi / 2, phi=0.1)) s.concatenate(natives.single_qubit[0].RX.create_sequence(theta=np.pi / 2, phi=0.2)) - s.append((qubit.probe.name, Delay(duration=s.duration))) - s.append((qubit.acquisition.name, Delay(duration=s.duration))) + s.append((qubit.probe, Delay(duration=s.duration))) + s.append((qubit.acquisition, Delay(duration=s.duration))) s.concatenate(natives.single_qubit[0].MZ.create_sequence()) # the delay sorting depends on PulseSequence.channels, which is a set, and it's @@ -187,7 +186,7 @@ def test_align_delay_measurement(platform: Platform, delay): target_sequence = PulseSequence() if delay > 0: - target_sequence.append((platform.qubits[0].probe.name, Delay(duration=delay))) + target_sequence.append((platform.qubits[0].probe, Delay(duration=delay))) target_sequence.concatenate(natives.single_qubit[0].MZ.create_sequence()) assert sequence == target_sequence assert len(sequence.acquisitions) == 1 @@ -201,9 +200,9 @@ def test_align_multiqubit(platform: Platform): circuit.add(gates.M(main, coupled)) sequence = compile_circuit(circuit, platform) - flux_duration = sequence.channel_duration(ChannelId.load(f"qubit_{coupled}/flux")) + flux_duration = sequence.channel_duration(f"qubit_{coupled}/flux") for q in (main, coupled): - probe_delay = next(iter(sequence.channel(ChannelId.load(f"qubit_{q}/probe")))) + probe_delay = next(iter(sequence.channel(f"qubit_{q}/probe"))) assert isinstance(probe_delay, Delay) assert flux_duration == probe_delay.duration @@ -228,12 +227,12 @@ def test_inactive_qubits(platform: Platform, joint: bool): natives.CZ.clear() sequence = compile_circuit(circuit, platform) + qm = platform.qubit(main)[1] + qc = platform.qubit(coupled)[1] + readouts = {qm.probe, qm.acquisition, qc.probe, qc.acquisition} + def no_measurement(seq: PulseSequence): - return [ - el - for el in seq - if el[0].channel_type not in (ChannelType.PROBE, ChannelType.ACQUISITION) - ] + return [el for el in seq if el[0] not in readouts] assert len(no_measurement(sequence)) == 1 @@ -252,12 +251,9 @@ def no_measurement(seq: PulseSequence): ) padded_seq = compile_circuit(circuit, platform) assert len(no_measurement(padded_seq)) == 3 - cdrive_delay = next(iter(padded_seq.channel(ChannelId.load(cdrive)))) + cdrive_delay = next(iter(padded_seq.channel(cdrive))) assert isinstance(cdrive_delay, Delay) - assert ( - cdrive_delay.duration - == next(iter(padded_seq.channel(ChannelId.load(mflux)))).duration - ) + assert cdrive_delay.duration == next(iter(padded_seq.channel(mflux))).duration def test_joint_split_equivalence(platform: Platform): @@ -297,5 +293,4 @@ def test_joint_split_equivalence(platform: Platform): "qubit_0/probe", "qubit_2/probe", ): - chid = ChannelId.load(ch) - assert list(joint_seq.channel(chid)) == list(split_seq.channel(chid)) + assert list(joint_seq.channel(ch)) == list(split_seq.channel(ch)) From 89db8bbbaded446d6977c657861f74e992d157e5 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 18:37:02 +0200 Subject: [PATCH 32/79] fix: Approximate fix of few pylint errors Due to the instruments transition to pydantic --- src/qibolab/instruments/bluefors.py | 19 ++++++++----------- src/qibolab/instruments/erasynth.py | 8 ++++++-- src/qibolab/instruments/qm/controller.py | 15 ++++++--------- src/qibolab/instruments/zhinst/executor.py | 2 +- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/qibolab/instruments/bluefors.py b/src/qibolab/instruments/bluefors.py index b38442ca4..b92ddc6fd 100644 --- a/src/qibolab/instruments/bluefors.py +++ b/src/qibolab/instruments/bluefors.py @@ -1,6 +1,7 @@ import socket import yaml +from pydantic import Field from qibo.config import log from qibolab.instruments.abstract import Instrument @@ -19,17 +20,13 @@ class TemperatureController(Instrument): print(temperature_value) """ - def __init__(self, name: str, address: str, port: int = 8888): - """Creation of the controller object. - - Args: - name (str): name of the instrument. - address (str): IP address of the board sending cryo temperature data. - port (int): port of the board sending cryo temperature data. - """ - super().__init__(name, address) - self.port = port - self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + address: str + """IP address of the board sending cryo temperature data.""" + port: int = 8888 + """Port of the board sending cryo temperature data.""" + client_socket: socket.socket = Field( + default_factory=lambda: socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ) def connect(self): """Connect to the socket.""" diff --git a/src/qibolab/instruments/erasynth.py b/src/qibolab/instruments/erasynth.py index 9f4b13652..7bf88dc9f 100644 --- a/src/qibolab/instruments/erasynth.py +++ b/src/qibolab/instruments/erasynth.py @@ -4,7 +4,7 @@ from qcodes_contrib_drivers.drivers.ERAInstruments import ERASynthPlusPlus from qibo.config import log -from qibolab.instruments.oscillator import LocalOscillator +from qibolab.instruments.oscillator import LocalOscillator, LocalOscillatorSettings RECONNECTION_ATTEMPTS = 10 """Number of times to attempt sending requests to the web server in case of @@ -128,7 +128,11 @@ class ERA(LocalOscillator): """ def __init__(self, name, address, ethernet=True, ref_osc_source=None): - super().__init__(name, address, ref_osc_source) + super().__init__( + name=name, + address=address, + settings=LocalOscillatorSettings(ref_osc_source=ref_osc_source), + ) self.ethernet = ethernet def create(self): diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 8d536ee3c..4168cde59 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -2,10 +2,11 @@ import tempfile import warnings from collections import defaultdict -from dataclasses import asdict, dataclass, field +from dataclasses import asdict, dataclass from pathlib import Path from typing import Optional +from pydantic import Field from qm import QuantumMachinesManager, SimulationConfig, generate_qua_script from qm.octave import QmOctaveConfig from qm.simulate.credentials import create_credentials @@ -107,7 +108,6 @@ def find_sweepers( return [s for ps in sweepers for s in ps if s.parameter is parameter] -@dataclass class QmController(Controller): """:class:`qibolab.instruments.abstract.Controller` object for controlling a Quantum Machines cluster. @@ -133,7 +133,7 @@ class QmController(Controller): """Dictionary containing the :class:`qibolab.instruments.qm.controller.Octave` instruments being used.""" - channels: dict[str, QmChannel] + channels: dict[ChannelId, QmChannel] bounds: str = "qm/bounds" """Maximum bounds used for batching in sequence unrolling.""" @@ -160,7 +160,7 @@ class QmController(Controller): is_connected: bool = False """Boolean that shows whether we are connected to the QM manager.""" - config: QmConfig = field(default_factory=QmConfig) + config: QmConfig = Field(default_factory=QmConfig) """Configuration dictionary required for pulse execution on the OPXs.""" simulation_duration: Optional[int] = None @@ -179,12 +179,9 @@ class QmController(Controller): Default is ``False``. """ - def __post_init__(self): - super().__init__(self.name, self.address) + def model_post_init(self, __context): # convert ``channels`` from list to dict - self.channels = { - str(channel.logical_channel.name): channel for channel in self.channels - } + self.channels = {channel.logical_channel: channel for channel in self.channels} if self.simulation_duration is not None: # convert simulation duration from ns to clock cycles diff --git a/src/qibolab/instruments/zhinst/executor.py b/src/qibolab/instruments/zhinst/executor.py index ede3c5dcd..afb14bf22 100644 --- a/src/qibolab/instruments/zhinst/executor.py +++ b/src/qibolab/instruments/zhinst/executor.py @@ -56,7 +56,7 @@ def __init__( time_of_flight=0.0, smearing=0.0, ): - super().__init__(name, None) + super().__init__(name=name, address=None) self.signal_map = {} "Signals to lines mapping" From 2620f92e6a02033ea5a369a953dac5e842ebe7f1 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 18:54:14 +0200 Subject: [PATCH 33/79] fix: Rename readout import in qm --- src/qibolab/instruments/qm/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 4168cde59..1feeaf1b0 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -16,7 +16,7 @@ from qibolab.execution_parameters import ExecutionParameters from qibolab.identifier import ChannelId from qibolab.instruments.abstract import Controller -from qibolab.pulses.pulse import Acquisition, Align, Delay, Pulse, _Readout +from qibolab.pulses import Acquisition, Align, Delay, Pulse, Readout from qibolab.sequence import PulseSequence from qibolab.sweeper import ParallelSweepers, Parameter, Sweeper from qibolab.unrolling import Bounds, unroll_sequences @@ -364,7 +364,7 @@ def register_acquisitions( """ acquisitions = {} for channel_id, readout in sequence.as_readouts: - if not isinstance(readout, _Readout): + if not isinstance(readout, Readout): continue if readout.probe.duration != readout.acquisition.duration: From 18dba163d1255399f893ce60f9a112fa79ccf120 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Thu, 22 Aug 2024 18:54:37 +0200 Subject: [PATCH 34/79] feat!: Drop acquisition attribute in probe channels --- src/qibolab/components/channels.py | 7 ------- src/qibolab/dummy/platform.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/qibolab/components/channels.py b/src/qibolab/components/channels.py index e385ad57f..e791c43e4 100644 --- a/src/qibolab/components/channels.py +++ b/src/qibolab/components/channels.py @@ -47,13 +47,6 @@ class IqChannel(Channel): None, if the channel does not have an LO, or it is not configurable. """ - acquisition: Optional[str] = None - """In case self is a readout channel this shall contain the name of the - corresponding acquire channel. - - FIXME: This is temporary solution to be able to generate acquisition commands on correct channel in drivers, - until we make acquire channels completely independent, and users start putting explicit acquisition commands in pulse sequence. - """ class AcquireChannel(Channel): diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index 068ff4dc6..f226cbadd 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -25,7 +25,7 @@ def create_dummy() -> Platform: f"qubit_{q}/acquisition", ) instrument.channels |= { - probe: IqChannel(mixer=None, lo=None, acquisition=acquisition), + probe: IqChannel(mixer=None, lo=None), acquisition: AcquireChannel(twpa_pump=pump.name, probe=probe), drive: IqChannel(mixer=None, lo=None), drive12: IqChannel(mixer=None, lo=None), From 805e492e0b5a7e68f27952935ed199e18a5a2aa1 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 23 Aug 2024 14:54:57 +0200 Subject: [PATCH 35/79] docs: Add usual imports in Sphinx configurations, to solve Pydantic conflicts --- doc/source/conf.py | 2 ++ doc/source/tutorials/calibration.rst | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 3e292c915..d013ba3b0 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -21,6 +21,8 @@ # TODO: the following is a workaround for Sphinx doctest, cf. # - https://github.com/qiboteam/qibolab/commit/e04a6ab # - https://github.com/pydantic/pydantic/discussions/7763 +import qibolab.instruments.dummy +import qibolab.instruments.oscillator import qibolab.instruments.zhinst # -- Project information ----------------------------------------------------- diff --git a/doc/source/tutorials/calibration.rst b/doc/source/tutorials/calibration.rst index 575fff15b..d55defa4d 100644 --- a/doc/source/tutorials/calibration.rst +++ b/doc/source/tutorials/calibration.rst @@ -51,7 +51,7 @@ around the pre-defined frequency. sweeper = Sweeper( parameter=Parameter.frequency, range=(f0 - 2e8, f0 + 2e8, 1e6), - channels=[qubit.probe.name], + channels=[qubit.probe], ) We then define the execution parameters and launch the experiment. @@ -75,7 +75,7 @@ In few seconds, the experiment will be finished and we can proceed to plot it. acq = sequence.acquisitions[0][1] amplitudes = magnitude(results[acq.id][0]) - frequencies = sweeper.values + frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.probe).frequency plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") @@ -132,10 +132,10 @@ complex pulse sequence. Therefore with start with that: sequence = PulseSequence( [ ( - qubit.drive.name, + qubit.drive, Pulse(duration=2000, amplitude=0.01, envelope=Gaussian(rel_sigma=5)), ), - (qubit.probe.name, Delay(duration=sequence.duration)), + (qubit.probe, Delay(duration=sequence.duration)), ] ) sequence.concatenate(natives.MZ.create_sequence()) @@ -145,7 +145,7 @@ complex pulse sequence. Therefore with start with that: sweeper = Sweeper( parameter=Parameter.frequency, range=(f0 - 2e8, f0 + 2e8, 1e6), - channels=[qubit.drive.name], + channels=[qubit.drive], ) Note that the drive pulse has been changed to match the characteristics required @@ -166,7 +166,7 @@ We can now proceed to launch on hardware: _, acq = next(iter(sequence.acquisitions)) amplitudes = magnitude(results[acq.id][0]) - frequencies = sweeper.values + frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.drive).frequency plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") @@ -235,7 +235,7 @@ and its impact on qubit states in the IQ plane. # create pulse sequence 1 and add pulses one_sequence = PulseSequence() one_sequence.concatenate(natives.RX.create_sequence()) - one_sequence.append((qubit.probe.name, Delay(duration=one_sequence.duration))) + one_sequence.append((qubit.probe, Delay(duration=one_sequence.duration))) one_sequence.concatenate(natives.MZ.create_sequence()) # create pulse sequence 2 and add pulses From 48f19dd3b956eb6edc2f7cd50e57935f5890e0c1 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 23 Aug 2024 15:02:46 +0200 Subject: [PATCH 36/79] docs: Propagate channel id update to documentation --- doc/source/getting-started/experiment.rst | 6 ++--- doc/source/main-documentation/qibolab.rst | 31 ++++++++++++----------- src/qibolab/platform/platform.py | 2 +- src/qibolab/sweeper.py | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/doc/source/getting-started/experiment.rst b/doc/source/getting-started/experiment.rst index 9370ef710..b9621107d 100644 --- a/doc/source/getting-started/experiment.rst +++ b/doc/source/getting-started/experiment.rst @@ -82,7 +82,7 @@ In this example, the qubit is controlled by a Zurich Instruments' SHFQC instrume return Platform( name=NAME, runcard=runcard, - instruments={controller.name: controller}, + instruments={controller: controller}, resonator_type="3D", ) @@ -225,7 +225,7 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her sweeper = Sweeper( parameter=Parameter.frequency, range=(f0 - 2e8, f0 + 2e8, 1e6), - channels=[qubit.probe.name], + channels=[qubit.probe], ) # perform the experiment using specific options @@ -241,7 +241,7 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her # plot the results amplitudes = magnitude(results[acq.id][0]) - frequencies = sweeper.values + frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.probe).frequency plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") diff --git a/doc/source/main-documentation/qibolab.rst b/doc/source/main-documentation/qibolab.rst index b72361b55..6ccc4bee9 100644 --- a/doc/source/main-documentation/qibolab.rst +++ b/doc/source/main-documentation/qibolab.rst @@ -40,15 +40,16 @@ We can easily access the names of channels and other components, and based on th .. testcode:: python - drive_channel = platform.qubits[0].drive - print(f"Drive channel name: {drive_channel.name}") - print(f"Drive frequency: {platform.config(str(drive_channel.name)).frequency}") + drive_channel_id = platform.qubits[0].drive + drive_channel = platform.channels[drive_channel_id] + print(f"Drive channel name: {drive_channel_id}") + print(f"Drive frequency: {platform.config(drive_channel_id).frequency}") drive_lo = drive_channel.lo if drive_lo is None: - print(f"Drive channel {drive_channel.name} does not use an LO.") + print(f"Drive channel {drive_channel_id} does not use an LO.") else: - print(f"Name of LO for channel {drive_channel.name} is {drive_lo}") + print(f"Name of LO for channel {drive_channel_id} is {drive_lo}") print(f"LO frequency: {platform.config(str(drive_lo)).frequency}") .. testoutput:: python @@ -71,7 +72,7 @@ Now we can create a simple sequence (again, without explicitly giving any qubit natives = platform.natives.single_qubit[0] ps.concatenate(natives.RX.create_sequence()) ps.concatenate(natives.RX.create_sequence(phi=np.pi / 2)) - ps.append((qubit.probe.name, Delay(duration=200))) + ps.append((qubit.probe, Delay(duration=200))) ps.concatenate(natives.MZ.create_sequence()) Now we can execute the sequence on hardware: @@ -316,7 +317,7 @@ Typical experiments may include both pre-defined pulses and new ones: sequence.concatenate(natives.RX.create_sequence()) sequence.append( ( - ChannelId.load("some/drive"), + "some/drive", Pulse(duration=10, amplitude=0.5, relative_phase=0, envelope=Rectangular()), ) ) @@ -399,9 +400,9 @@ A tipical resonator spectroscopy experiment could be defined with: sweepers = [ Sweeper( parameter=Parameter.frequency, - values=platform.config(str(qubit.probe.name)).frequency + values=platform.config(qubit.probe).frequency + np.arange(-200_000, +200_000, 1), # define an interval of swept values - channels=[qubit.probe.name], + channels=[qubit.probe], ) for qubit in platform.qubits.values() ] @@ -429,19 +430,19 @@ For example: natives = platform.natives.single_qubit[0] sequence = PulseSequence() sequence.concatenate(natives.RX.create_sequence()) - sequence.append((qubit.probe.name, Delay(duration=sequence.duration))) + sequence.append((qubit.probe, Delay(duration=sequence.duration))) sequence.concatenate(natives.MZ.create_sequence()) f0 = platform.config(str(qubit.drive.name)).frequency sweeper_freq = Sweeper( parameter=Parameter.frequency, range=(f0 - 100_000, f0 + 100_000, 10_000), - channels=[qubit.drive.name], + channels=[qubit.drive], ) sweeper_amp = Sweeper( parameter=Parameter.amplitude, range=(0, 0.43, 0.3), - pulses=[next(iter(sequence.channel(qubit.drive.name)))], + pulses=[next(iter(sequence.channel(qubit.drive)))], ) results = platform.execute([sequence], options, [[sweeper_freq], [sweeper_amp]]) @@ -526,7 +527,7 @@ Let's now delve into a typical use case for result objects within the qibolab fr sequence = PulseSequence() sequence.concatenate(natives.RX.create_sequence()) - sequence.append((qubit.probe.name, Delay(duration=sequence.duration))) + sequence.append((qubit.probe, Delay(duration=sequence.duration))) sequence.concatenate(natives.MZ.create_sequence()) options = ExecutionParameters( @@ -554,12 +555,12 @@ The shape of the values of an integreted acquisition with 2 sweepers will be: sweeper1 = Sweeper( parameter=Parameter.frequency, range=(f0 - 100_000, f0 + 100_000, 1), - channels=[qubit.drive.name], + channels=[qubit.drive], ) sweeper2 = Sweeper( parameter=Parameter.frequency, range=(f0 - 200_000, f0 + 200_000, 1), - channels=[qubit.probe.name], + channels=[qubit.probe], ) shape = (options.nshots, len(sweeper1.values), len(sweeper2.values)) diff --git a/src/qibolab/platform/platform.py b/src/qibolab/platform/platform.py index 173ce708a..f35ac34bf 100644 --- a/src/qibolab/platform/platform.py +++ b/src/qibolab/platform/platform.py @@ -246,7 +246,7 @@ def execute( Sweeper( parameter=Parameter.frequency, values=parameter_range, - channels=[qubit.probe.name], + channels=[qubit.probe], ) ] platform.execute([sequence], ExecutionParameters(), [sweeper]) diff --git a/src/qibolab/sweeper.py b/src/qibolab/sweeper.py index 20587e806..96cd0696d 100644 --- a/src/qibolab/sweeper.py +++ b/src/qibolab/sweeper.py @@ -64,7 +64,7 @@ class Sweeper(Model): sequence = natives.MZ.create_sequence() parameter_range = np.random.randint(10, size=10) sweeper = Sweeper( - parameter=Parameter.frequency, values=parameter_range, channels=[qubit.probe.name] + parameter=Parameter.frequency, values=parameter_range, channels=[qubit.probe] ) platform.execute([sequence], ExecutionParameters(), [[sweeper]]) From ceb859ecb2b37ae1c5c3f351f81033a2846a0165 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 23 Aug 2024 15:04:26 +0200 Subject: [PATCH 37/79] docs: Propagate channel id update to documentation --- doc/source/tutorials/lab.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorials/lab.rst b/doc/source/tutorials/lab.rst index 0c35d5d54..d1bb66cc3 100644 --- a/doc/source/tutorials/lab.rst +++ b/doc/source/tutorials/lab.rst @@ -48,8 +48,8 @@ using different Qibolab primitives. # define configuration for channels configs = {} - configs[str(qubit.drive.name)] = IqConfig(frequency=3e9) - configs[str(qubit.probe.name)] = IqConfig(frequency=7e9) + configs[qubit.drive.name] = IqConfig(frequency=3e9) + configs[qubit.probe.name] = IqConfig(frequency=7e9) # create sequence that drives qubit from state 0 to 1 drive_seq = PulseSequence( From ed36bdc3c73af5204bb73bae1a59a9a2985b52bf Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 23 Aug 2024 16:42:01 +0200 Subject: [PATCH 38/79] feat: Add readout constructor from probe pulse --- src/qibolab/pulses/pulse.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/qibolab/pulses/pulse.py b/src/qibolab/pulses/pulse.py index f95ee52d5..2b03b3e58 100644 --- a/src/qibolab/pulses/pulse.py +++ b/src/qibolab/pulses/pulse.py @@ -117,6 +117,14 @@ class Readout(_PulseLike): acquisition: Acquisition probe: Pulse + @classmethod + def from_probe(cls, probe: Pulse): + """Create a whole readout operation from its probe pulse. + + The acquisition is made to match the same probe duration. + """ + return cls(acquisition=Acquisition(duration=probe.duration), probe=probe) + @property def duration(self) -> float: """Duration in ns.""" From 6f1a608a69e21500b94b63e1253dedb18e264b36 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 23 Aug 2024 19:18:41 +0200 Subject: [PATCH 39/79] fix: Convert dummy MZ natives to readout operations --- src/qibolab/dummy/parameters.json | 160 +++++++++++++++--------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/src/qibolab/dummy/parameters.json b/src/qibolab/dummy/parameters.json index e427c9ca1..e9e6a96f5 100644 --- a/src/qibolab/dummy/parameters.json +++ b/src/qibolab/dummy/parameters.json @@ -186,25 +186,25 @@ ] ], "MZ": [ - [ - "qubit_0/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - }, - "relative_phase": 0.0, - "kind": "pulse" - } - ], [ "qubit_0/acquisition", { - "kind": "acquisition", - "duration": 2000.0 + "kind": "readout", + "acquisition": { + "kind": "acquisition", + "duration": 2000.0 + }, + "probe": { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0, + "kind": "pulse" + } } ] ], @@ -244,25 +244,25 @@ ] ], "MZ": [ - [ - "qubit_1/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - }, - "relative_phase": 0.0, - "kind": "pulse" - } - ], [ "qubit_1/acquisition", { - "kind": "acquisition", - "duration": 2000.0 + "kind": "readout", + "acquisition": { + "kind": "acquisition", + "duration": 2000.0 + }, + "probe": { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0, + "kind": "pulse" + } } ] ], @@ -301,25 +301,25 @@ ] ], "MZ": [ - [ - "qubit_2/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - }, - "relative_phase": 0.0, - "kind": "pulse" - } - ], [ "qubit_2/acquisition", { - "kind": "acquisition", - "duration": 2000.0 + "kind": "readout", + "acquisition": { + "kind": "acquisition", + "duration": 2000.0 + }, + "probe": { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0, + "kind": "pulse" + } } ] ], @@ -359,25 +359,25 @@ ] ], "MZ": [ - [ - "qubit_3/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - }, - "relative_phase": 0.0, - "kind": "pulse" - } - ], [ "qubit_3/acquisition", { - "kind": "acquisition", - "duration": 2000.0 + "kind": "readout", + "acquisition": { + "kind": "acquisition", + "duration": 2000.0 + }, + "probe": { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0, + "kind": "pulse" + } } ] ], @@ -417,25 +417,25 @@ ] ], "MZ": [ - [ - "qubit_4/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - }, - "relative_phase": 0.0, - "kind": "pulse" - } - ], [ "qubit_4/acquisition", { - "kind": "acquisition", - "duration": 2000.0 + "kind": "readout", + "acquisition": { + "kind": "acquisition", + "duration": 2000.0 + }, + "probe": { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0, + "kind": "pulse" + } } ] ], From 3f88603fc3dd019a7b81ae96d08c5fe997085f9b Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 23 Aug 2024 19:38:31 +0200 Subject: [PATCH 40/79] test: Fix (some) tests due to readout upgrade --- tests/conftest.py | 4 ++-- tests/test_compilers_default.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0d443be63..de5ba56cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -147,8 +147,8 @@ def wrapped( if sequence is None: qd_seq = natives.RX.create_sequence() probe_seq = natives.MZ.create_sequence() - probe_pulse = probe_seq[0][1] - acq = probe_seq[1][1] + probe_pulse = probe_seq[0][1].probe + acq = probe_seq[0][1].acquisition wrapped.acquisition_duration = acq.duration sequence = PulseSequence() sequence.concatenate(qd_seq) diff --git a/tests/test_compilers_default.py b/tests/test_compilers_default.py index f65e95129..9737a0628 100644 --- a/tests/test_compilers_default.py +++ b/tests/test_compilers_default.py @@ -54,7 +54,7 @@ def test_compile(platform: Platform, gateargs): nqubits = platform.nqubits circuit = generate_circuit_with_gate(nqubits, *gateargs) sequence = compile_circuit(circuit, platform) - assert len(sequence.channels) == nqubits * int(gateargs[0] != gates.I) + nqubits * 2 + assert len(sequence.channels) == nqubits * int(gateargs[0] != gates.I) + nqubits * 1 def test_compile_two_gates(platform: Platform): @@ -78,7 +78,7 @@ def test_measurement(platform: Platform): circuit.add(gates.M(*qubits)) sequence = compile_circuit(circuit, platform) - assert len(sequence.channels) == 2 * nqubits + assert len(sequence.channels) == 1 * nqubits assert len(sequence.acquisitions) == 1 * nqubits @@ -152,7 +152,7 @@ def test_add_measurement_to_sequence(platform: Platform): sequence = compile_circuit(circuit, platform) qubit = platform.qubits[0] - assert len(sequence.channels) == 3 + assert len(sequence.channels) == 2 assert len(list(sequence.channel(qubit.drive))) == 2 assert len(list(sequence.channel(qubit.probe))) == 2 # include delay @@ -202,7 +202,7 @@ def test_align_multiqubit(platform: Platform): sequence = compile_circuit(circuit, platform) flux_duration = sequence.channel_duration(f"qubit_{coupled}/flux") for q in (main, coupled): - probe_delay = next(iter(sequence.channel(f"qubit_{q}/probe"))) + probe_delay = next(iter(sequence.channel(f"qubit_{q}/acquisition"))) assert isinstance(probe_delay, Delay) assert flux_duration == probe_delay.duration From b43763fafd2edad053313de927018ef910eb2a28 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Sat, 24 Aug 2024 02:00:36 +0200 Subject: [PATCH 41/79] test: Fix final tests for readout migration --- tests/test_compilers_default.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_compilers_default.py b/tests/test_compilers_default.py index 9737a0628..745c22000 100644 --- a/tests/test_compilers_default.py +++ b/tests/test_compilers_default.py @@ -66,9 +66,9 @@ def test_compile_two_gates(platform: Platform): sequence = compile_circuit(circuit, platform) qubit = platform.qubits[0] - assert len(sequence.channels) == 3 + assert len(sequence.channels) == 2 assert len(list(sequence.channel(qubit.drive))) == 2 - assert len(list(sequence.channel(qubit.probe))) == 2 # includes delay + assert len(list(sequence.channel(qubit.acquisition))) == 2 # includes delay def test_measurement(platform: Platform): @@ -154,12 +154,11 @@ def test_add_measurement_to_sequence(platform: Platform): qubit = platform.qubits[0] assert len(sequence.channels) == 2 assert len(list(sequence.channel(qubit.drive))) == 2 - assert len(list(sequence.channel(qubit.probe))) == 2 # include delay + assert len(list(sequence.channel(qubit.acquisition))) == 2 # include delay s = PulseSequence() s.concatenate(natives.single_qubit[0].RX.create_sequence(theta=np.pi / 2, phi=0.1)) s.concatenate(natives.single_qubit[0].RX.create_sequence(theta=np.pi / 2, phi=0.2)) - s.append((qubit.probe, Delay(duration=s.duration))) s.append((qubit.acquisition, Delay(duration=s.duration))) s.concatenate(natives.single_qubit[0].MZ.create_sequence()) @@ -186,7 +185,7 @@ def test_align_delay_measurement(platform: Platform, delay): target_sequence = PulseSequence() if delay > 0: - target_sequence.append((platform.qubits[0].probe, Delay(duration=delay))) + target_sequence.append((platform.qubits[0].acquisition, Delay(duration=delay))) target_sequence.concatenate(natives.single_qubit[0].MZ.create_sequence()) assert sequence == target_sequence assert len(sequence.acquisitions) == 1 From 9e5a069447a4e5658d3fb87d2c53d502b2e42c12 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 00:39:32 +0200 Subject: [PATCH 42/79] refactor: Export variables to postpone compatibility breaking --- src/qibolab/qubits.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index fc12367e7..6f658ff7c 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -2,7 +2,10 @@ from pydantic import ConfigDict, Field -from .identifier import ChannelId, TransitionId +# TODO: the unused import are there because Qibocal is still importing them from here +# since the export scheme will be reviewed, it should be changed at that time, removing +# the unused ones from here +from .identifier import ChannelId, TransitionId, QubitId, QubitPairId from .serialize import Model From 00addf31a1203c1b417a7cb9cedcee21fc25172b Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 00:40:18 +0200 Subject: [PATCH 43/79] feat: Support device and port path in general channels --- src/qibolab/components/channels.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/qibolab/components/channels.py b/src/qibolab/components/channels.py index e791c43e4..f59f31f33 100644 --- a/src/qibolab/components/channels.py +++ b/src/qibolab/components/channels.py @@ -20,6 +20,7 @@ from typing import Optional +from qibolab.identifier import ChannelId from qibolab.serialize import Model __all__ = ["Channel", "DcChannel", "IqChannel", "AcquireChannel"] @@ -28,6 +29,15 @@ class Channel(Model): """Channel to communicate with the qubit.""" + device: str = "" + """Name of the device.""" + path: str = "" + """Physical port addresss within the device.""" + + @property + def port(self) -> int: + return int(self.path) + class DcChannel(Channel): """Channel that can be used to send DC pulses.""" @@ -55,7 +65,7 @@ class AcquireChannel(Channel): None, if there is no TWPA, or it is not configurable. """ - probe: Optional[str] = None + probe: Optional[ChannelId] = None """Name of the corresponding measure/probe channel. FIXME: This is temporary solution to be able to relate acquisition channel to corresponding probe channel wherever needed in drivers, From 59773d81f526950c2382015f079670c929ee3c99 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 00:41:32 +0200 Subject: [PATCH 44/79] feat!: Remove qm-specific channels, and improve re-export --- .../instruments/qm/components/__init__.py | 5 ++++- .../instruments/qm/components/channel.py | 19 ------------------- 2 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 src/qibolab/instruments/qm/components/channel.py diff --git a/src/qibolab/instruments/qm/components/__init__.py b/src/qibolab/instruments/qm/components/__init__.py index f1fd439eb..0138aa43c 100644 --- a/src/qibolab/instruments/qm/components/__init__.py +++ b/src/qibolab/instruments/qm/components/__init__.py @@ -1,2 +1,5 @@ -from .channel import * from .configs import * +from . import configs + +__all__ = [] +__all__ += configs.__all__ diff --git a/src/qibolab/instruments/qm/components/channel.py b/src/qibolab/instruments/qm/components/channel.py deleted file mode 100644 index 6aabc3c2a..000000000 --- a/src/qibolab/instruments/qm/components/channel.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass - -from qibolab.components import Channel - -__all__ = [ - "QmChannel", -] - - -@dataclass(frozen=True) -class QmChannel: - """Channel for Quantum Machines devices.""" - - logical_channel: Channel - """Corresponding logical channel.""" - device: str - """Name of the device.""" - port: int - """Number of port.""" From f0d620589e3a03da9af4eef3216997e0f1c42385 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 00:43:57 +0200 Subject: [PATCH 45/79] refactor: Improve some qm type hints --- src/qibolab/instruments/qm/config/elements.py | 24 ++++++++----------- src/qibolab/instruments/qm/config/pulses.py | 6 ++--- .../instruments/qm/program/acquisition.py | 2 +- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/qibolab/instruments/qm/config/elements.py b/src/qibolab/instruments/qm/config/elements.py index a08b7be89..526dd831f 100644 --- a/src/qibolab/instruments/qm/config/elements.py +++ b/src/qibolab/instruments/qm/config/elements.py @@ -3,19 +3,15 @@ import numpy as np -from ..components import QmChannel +from qibolab.components.channels import Channel -__all__ = [ - "DcElement", - "RfOctaveElement", - "AcquireOctaveElement", - "Element", -] +__all__ = ["DcElement", "RfOctaveElement", "AcquireOctaveElement", "Element"] def iq_imbalance(g, phi): - """Creates the correction matrix for the mixer imbalance caused by the gain - and phase imbalances. + """Create the correction matrix for the mixer imbalance. + + Mixer imbalance is caused by the gain and phase imbalances. More information here: https://docs.qualang.io/libs/examples/mixer-calibration/#non-ideal-mixer @@ -45,7 +41,7 @@ class OutputSwitch: """ -def _to_port(channel: QmChannel) -> dict[str, tuple[str, int]]: +def _to_port(channel: Channel) -> dict[str, tuple[str, int]]: """Convert a channel to the port dictionary required for the QUA config.""" return {"port": (channel.device, channel.port)} @@ -62,7 +58,7 @@ class DcElement: operations: dict[str, str] = field(default_factory=dict) @classmethod - def from_channel(cls, channel: QmChannel): + def from_channel(cls, channel: Channel): return cls(_to_port(channel)) @@ -75,7 +71,7 @@ class RfOctaveElement: @classmethod def from_channel( - cls, channel: QmChannel, connectivity: str, intermediate_frequency: int + cls, channel: Channel, connectivity: str, intermediate_frequency: int ): return cls( _to_port(channel), @@ -97,8 +93,8 @@ class AcquireOctaveElement: @classmethod def from_channel( cls, - probe_channel: QmChannel, - acquire_channel: QmChannel, + probe_channel: Channel, + acquire_channel: Channel, connectivity: str, intermediate_frequency: int, time_of_flight: int, diff --git a/src/qibolab/instruments/qm/config/pulses.py b/src/qibolab/instruments/qm/config/pulses.py index 0635865a1..34340e9df 100644 --- a/src/qibolab/instruments/qm/config/pulses.py +++ b/src/qibolab/instruments/qm/config/pulses.py @@ -42,7 +42,7 @@ class ConstantWaveform: type: str = "constant" @classmethod - def from_pulse(cls, pulse: Pulse): + def from_pulse(cls, pulse: Pulse) -> dict[str, "Waveform"]: phase = wrap_phase(pulse.relative_phase) voltage_amp = pulse.amplitude * MAX_VOLTAGE_OUTPUT return { @@ -57,7 +57,7 @@ class ArbitraryWaveform: type: str = "arbitrary" @classmethod - def from_pulse(cls, pulse: Pulse): + def from_pulse(cls, pulse: Pulse) -> dict[str, "Waveform"]: original_waveforms = pulse.envelopes(SAMPLING_RATE) * MAX_VOLTAGE_OUTPUT rotated_waveforms = rotate(original_waveforms, pulse.relative_phase) new_duration = baked_duration(pulse.duration) @@ -72,7 +72,7 @@ def from_pulse(cls, pulse: Pulse): Waveform = Union[ConstantWaveform, ArbitraryWaveform] -def waveforms_from_pulse(pulse: Pulse) -> Waveform: +def waveforms_from_pulse(pulse: Pulse) -> dict[str, Waveform]: """Register QM waveforms for a given pulse.""" needs_baking = pulse.duration < 16 or pulse.duration % 4 != 0 wvtype = ( diff --git a/src/qibolab/instruments/qm/program/acquisition.py b/src/qibolab/instruments/qm/program/acquisition.py index 624c67595..b38e6038a 100644 --- a/src/qibolab/instruments/qm/program/acquisition.py +++ b/src/qibolab/instruments/qm/program/acquisition.py @@ -43,7 +43,7 @@ class Acquisition(ABC): element: str """Element from QM ``config`` that the pulse will be applied on.""" average: bool - keys: list[str] = field(default_factory=list) + keys: list[int] = field(default_factory=list) @property def name(self): From 1d9c7302bcc570fda52333ae2df5b4998a84b690 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 00:44:31 +0200 Subject: [PATCH 46/79] refactor: Improve dict subclass, and its typing --- src/qibolab/instruments/qm/config/devices.py | 43 +++++++------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/src/qibolab/instruments/qm/config/devices.py b/src/qibolab/instruments/qm/config/devices.py index 598b3d1b0..aa017e083 100644 --- a/src/qibolab/instruments/qm/config/devices.py +++ b/src/qibolab/instruments/qm/config/devices.py @@ -1,16 +1,12 @@ +from collections import UserDict from dataclasses import dataclass, field +from typing import Any, Generic, TypeVar from qibolab.components.configs import OscillatorConfig from ..components import OpxOutputConfig, QmAcquisitionConfig -__all__ = [ - "AnalogOutput", - "OctaveOutput", - "OctaveInput", - "Controller", - "Octave", -] +__all__ = ["AnalogOutput", "OctaveOutput", "OctaveInput", "Controller", "Octave"] DEFAULT_INPUTS = {"1": {}, "2": {}} @@ -20,15 +16,17 @@ calibration when using Octaves. """ +V = TypeVar("V") -class PortDict(dict): + +class PortDict(Generic[V], UserDict[str, V]): """Dictionary that automatically converts keys to strings. Used to register input and output ports to controllers and Octaves in the QUA config. """ - def __setitem__(self, key, value): + def __setitem__(self, key: Any, value: V): super().__setitem__(str(key), value) @@ -39,10 +37,7 @@ class AnalogOutput: @classmethod def from_config(cls, config: OpxOutputConfig): - return cls( - offset=config.offset, - filter=config.filter, - ) + return cls(offset=config.offset, filter=config.filter) @dataclass(frozen=True) @@ -52,10 +47,7 @@ class AnalogInput: @classmethod def from_config(cls, config: QmAcquisitionConfig): - return cls( - offset=config.offset, - gain_db=config.gain, - ) + return cls(offset=config.offset, gain_db=config.gain) @dataclass(frozen=True) @@ -67,10 +59,7 @@ class OctaveOutput: @classmethod def from_config(cls, config: OscillatorConfig): - return cls( - LO_frequency=config.frequency, - gain=config.power, - ) + return cls(LO_frequency=config.frequency, gain=config.power) @dataclass(frozen=True) @@ -83,11 +72,9 @@ class OctaveInput: @dataclass class Controller: - analog_outputs: PortDict[str, dict[str, AnalogOutput]] = field( - default_factory=PortDict - ) - digital_outputs: PortDict[str, dict[str, dict]] = field(default_factory=PortDict) - analog_inputs: PortDict[str, dict[str, AnalogInput]] = field( + analog_outputs: PortDict[dict[str, AnalogOutput]] = field(default_factory=PortDict) + digital_outputs: PortDict[dict[str, dict]] = field(default_factory=PortDict) + analog_inputs: PortDict[dict[str, AnalogInput]] = field( default_factory=lambda: PortDict(DEFAULT_INPUTS) ) @@ -107,5 +94,5 @@ def add_octave_input(self, port: int, config: QmAcquisitionConfig): @dataclass class Octave: connectivity: str - RF_outputs: PortDict[str, dict[str, OctaveOutput]] = field(default_factory=PortDict) - RF_inputs: PortDict[str, dict[str, OctaveInput]] = field(default_factory=PortDict) + RF_outputs: PortDict[dict[str, OctaveOutput]] = field(default_factory=PortDict) + RF_inputs: PortDict[dict[str, OctaveInput]] = field(default_factory=PortDict) From 69097919509eeec3d56415fc0b5feb42d70807ff Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 00:45:05 +0200 Subject: [PATCH 47/79] fix: Propagate qmchannel deprecation to its configs --- src/qibolab/instruments/qm/config/config.py | 47 ++++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/qibolab/instruments/qm/config/config.py b/src/qibolab/instruments/qm/config/config.py index e85033252..7ed1e6127 100644 --- a/src/qibolab/instruments/qm/config/config.py +++ b/src/qibolab/instruments/qm/config/config.py @@ -1,13 +1,15 @@ from dataclasses import dataclass, field from typing import Optional, Union +from qibolab.components.channels import AcquireChannel, DcChannel, IqChannel from qibolab.components.configs import IqConfig, OscillatorConfig +from qibolab.identifier import ChannelId from qibolab.pulses import Pulse -from ..components import OpxOutputConfig, QmAcquisitionConfig, QmChannel -from .devices import * -from .elements import * -from .pulses import * +from ..components import OpxOutputConfig, QmAcquisitionConfig +from .devices import AnalogOutput, Controller, Octave, OctaveInput, OctaveOutput +from .elements import AcquireOctaveElement, DcElement, Element, RfOctaveElement +from .pulses import QmAcquisition, QmPulse, Waveform, operation, waveforms_from_pulse __all__ = ["QmConfig"] @@ -48,15 +50,19 @@ def add_octave(self, device: str, connectivity: str): self.add_controller(connectivity) self.octaves[device] = Octave(connectivity) - def configure_dc_line(self, channel: QmChannel, config: OpxOutputConfig): + def configure_dc_line( + self, id: ChannelId, channel: DcChannel, config: OpxOutputConfig + ): controller = self.controllers[channel.device] controller.analog_outputs[channel.port] = AnalogOutput.from_config(config) - self.elements[str(channel.logical_channel.name)] = DcElement.from_channel( - channel - ) + self.elements[id] = DcElement.from_channel(channel) def configure_iq_line( - self, channel: QmChannel, config: IqConfig, lo_config: OscillatorConfig + self, + id: ChannelId, + channel: IqChannel, + config: IqConfig, + lo_config: OscillatorConfig, ): port = channel.port octave = self.octaves[channel.device] @@ -64,14 +70,15 @@ def configure_iq_line( self.controllers[octave.connectivity].add_octave_output(port) intermediate_frequency = config.frequency - lo_config.frequency - self.elements[str(channel.logical_channel.name)] = RfOctaveElement.from_channel( + self.elements[id] = RfOctaveElement.from_channel( channel, octave.connectivity, intermediate_frequency ) def configure_acquire_line( self, - acquire_channel: QmChannel, - probe_channel: QmChannel, + id: ChannelId, + acquire_channel: AcquireChannel, + probe_channel: IqChannel, acquire_config: QmAcquisitionConfig, probe_config: IqConfig, lo_config: OscillatorConfig, @@ -87,15 +94,13 @@ def configure_acquire_line( self.controllers[octave.connectivity].add_octave_output(port) intermediate_frequency = probe_config.frequency - lo_config.frequency - self.elements[str(probe_channel.logical_channel.name)] = ( - AcquireOctaveElement.from_channel( - probe_channel, - acquire_channel, - octave.connectivity, - intermediate_frequency, - time_of_flight=acquire_config.delay, - smearing=acquire_config.smearing, - ) + self.elements[id] = AcquireOctaveElement.from_channel( + probe_channel, + acquire_channel, + octave.connectivity, + intermediate_frequency, + time_of_flight=acquire_config.delay, + smearing=acquire_config.smearing, ) def register_waveforms( From 39b324be96eaa37142b819795ccb53d2729068a6 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 10:17:27 +0200 Subject: [PATCH 48/79] fix: Replace qm-specific channels with general ones in the controller --- src/qibolab/instruments/qm/controller.py | 147 +++++++++++------------ 1 file changed, 72 insertions(+), 75 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 1feeaf1b0..cdc716508 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -1,3 +1,4 @@ +from os import PathLike import shutil import tempfile import warnings @@ -7,6 +8,11 @@ from typing import Optional from pydantic import Field +from qibolab.components.configs import AcquisitionConfig, IqConfig, OscillatorConfig +from qibolab.instruments.qm.components.configs import ( + OpxOutputConfig, + QmAcquisitionConfig, +) from qm import QuantumMachinesManager, SimulationConfig, generate_qua_script from qm.octave import QmOctaveConfig from qm.simulate.credentials import create_credentials @@ -21,7 +27,6 @@ from qibolab.sweeper import ParallelSweepers, Parameter, Sweeper from qibolab.unrolling import Bounds, unroll_sequences -from .components import QmChannel from .config import SAMPLING_RATE, QmConfig, operation from .program import ExecutionArguments, create_acquisition, program from .program.sweepers import check_frequency_bandwidth, sweeper_amplitude @@ -133,15 +138,14 @@ class QmController(Controller): """Dictionary containing the :class:`qibolab.instruments.qm.controller.Octave` instruments being used.""" - channels: dict[ChannelId, QmChannel] bounds: str = "qm/bounds" """Maximum bounds used for batching in sequence unrolling.""" - calibration_path: Optional[str] = None + calibration_path: Optional[PathLike] = None """Path to the JSON file that contains the mixer calibration.""" write_calibration: bool = False """Require writing permissions on calibration DB.""" - _calibration_path: Optional[str] = None + _calibration_path: Optional[PathLike] = None """The calibration path for internal use. Cf. :attr:`calibration_path` for its role. This might be set to a different one @@ -180,9 +184,6 @@ class QmController(Controller): """ def model_post_init(self, __context): - # convert ``channels`` from list to dict - self.channels = {channel.logical_channel: channel for channel in self.channels} - if self.simulation_duration is not None: # convert simulation duration from ns to clock cycles self.simulation_duration //= 4 @@ -243,55 +244,57 @@ def configure_device(self, device: str): else: self.config.add_controller(device) - def configure_channel(self, channel: QmChannel, configs: dict[str, Config]): + def configure_channel(self, channel: ChannelId, configs: dict[str, Config]): """Add element (QM version of channel) in the config.""" - logical_channel = channel.logical_channel - channel_config = configs[str(logical_channel.name)] - self.configure_device(channel.device) - - if isinstance(logical_channel, DcChannel): - self.config.configure_dc_line(channel, channel_config) + config = configs[channel] + ch = self.channels[channel] + self.configure_device(ch.device) + + if isinstance(ch, DcChannel): + assert isinstance(config, OpxOutputConfig) + self.config.configure_dc_line(channel, ch, config) + + elif isinstance(ch, IqChannel): + assert ch.lo is not None + assert isinstance(config, IqConfig) + lo_config = configs[ch.lo] + assert isinstance(lo_config, OscillatorConfig) + self.config.configure_iq_line(channel, ch, config, lo_config) + + elif isinstance(ch, AcquireChannel): + assert ch.probe is not None + assert isinstance(config, QmAcquisitionConfig) + probe = self.channels[ch.probe] + probe_config = configs[ch.probe] + assert isinstance(probe, IqChannel) + assert isinstance(probe_config, IqConfig) + assert probe.lo is not None + lo_config = configs[probe.lo] + assert isinstance(lo_config, OscillatorConfig) + self.configure_device(ch.device) + self.config.configure_acquire_line( + channel, ch, probe, config, probe_config, lo_config + ) - elif isinstance(logical_channel, IqChannel): - lo_config = configs[logical_channel.lo] - if logical_channel.acquisition is None: - self.config.configure_iq_line(channel, channel_config, lo_config) + else: + raise TypeError(f"Unknown channel type: {type(ch)}.") - else: - acquisition = logical_channel.acquisition - acquire_channel = self.channels[acquisition] - self.configure_device(acquire_channel.device) - self.config.configure_acquire_line( - acquire_channel, - channel, - configs[acquisition], - channel_config, - lo_config, - ) + def configure_channels(self, configs: dict[str, Config], channels: set[ChannelId]): + """Register channels in the sequence in the QM ``config``.""" + for id in channels: + self.configure_channel(id, configs) - elif not isinstance(logical_channel, AcquireChannel): - raise TypeError(f"Unknown channel type: {type(logical_channel)}.") + def register_pulse(self, channel: ChannelId, pulse: Pulse) -> str: + """Add pulse in the QM ``config``. - def configure_channels( - self, - configs: dict[str, Config], - channels: set[ChannelId], - ): - """Register channels participating in the sequence in the QM - ``config``.""" - for channel_id in channels: - channel = self.channels[str(channel_id)] - self.configure_channel(channel, configs) - - def register_pulse(self, channel: Channel, pulse: Pulse) -> str: - """Add pulse in the QM ``config`` and return corresponding - operation.""" - name = str(channel.name) - if isinstance(channel, DcChannel): - return self.config.register_dc_pulse(name, pulse) - if channel.acquisition is None: - return self.config.register_iq_pulse(name, pulse) - return self.config.register_acquisition_pulse(name, pulse) + And return corresponding operation. + """ + ch = self.channels[channel] + if isinstance(ch, DcChannel): + return self.config.register_dc_pulse(channel, pulse) + if isinstance(ch, IqChannel): + return self.config.register_iq_pulse(channel, pulse) + return self.config.register_acquisition_pulse(channel, pulse) def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): """Adds all pulses except measurements of a given sequence in the QM @@ -300,15 +303,15 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): Returns: acquisitions (dict): Map from measurement instructions to acquisition objects. """ - for channel_id, pulse in sequence: + for id, pulse in sequence: if hasattr(pulse, "duration") and not pulse.duration.is_integer(): raise ValueError( f"Quantum Machines cannot play pulse with duration {pulse.duration}. " "Only integer duration in ns is supported." ) + if isinstance(pulse, Pulse): - channel = self.channels[str(channel_id)].logical_channel - self.register_pulse(channel, pulse) + self.register_pulse(id, pulse) def register_duration_sweeper_pulses( self, args: ExecutionArguments, sweeper: Sweeper @@ -323,7 +326,7 @@ def register_duration_sweeper_pulses( params = args.parameters[operation(pulse)] channel_ids = args.sequence.pulse_channels(pulse.id) - channel = self.channels[str(channel_ids[0])].logical_channel + channel = self.channels[channel_ids[0]].logical_channel original_pulse = ( pulse if params.amplitude_pulse is None else params.amplitude_pulse ) @@ -340,11 +343,10 @@ def register_amplitude_sweeper_pulses( Needed when sweeping amplitude because the original amplitude may not sufficient to reach all the sweeper values. """ - new_op = None amplitude = sweeper_amplitude(sweeper.values) for pulse in sweeper.pulses: channel_ids = args.sequence.pulse_channels(pulse.id) - channel = self.channels[str(channel_ids[0])].logical_channel + channel = self.channels[channel_ids[0]].logical_channel sweep_pulse = pulse.model_copy(update={"amplitude": amplitude}) params = args.parameters[operation(pulse)] @@ -357,13 +359,13 @@ def register_acquisitions( sequence: PulseSequence, options: ExecutionParameters, ): - """Adds all measurements of a given sequence in the QM ``config``. + """Add all measurements of a given sequence in the QM ``config``. Returns: acquisitions (dict): Map from measurement instructions to acquisition objects. """ acquisitions = {} - for channel_id, readout in sequence.as_readouts: + for channel_id, readout in sequence: if not isinstance(readout, Readout): continue @@ -372,23 +374,18 @@ def register_acquisitions( "Quantum Machines does not support acquisition with different duration than probe." ) - channel_name = str(channel_id) - channel = self.channels[channel_name].logical_channel - op = self.config.register_acquisition_pulse(channel_name, readout.probe) + op = self.config.register_acquisition_pulse(channel_id, readout.probe) - acq_config = configs[channel.acquisition] + acq_config = configs[channel_id] + assert isinstance(acq_config, QmAcquisitionConfig) self.config.register_integration_weights( - channel_name, readout.duration, acq_config.kernel + channel_id, readout.duration, acq_config.kernel ) - if (op, channel_name) in acquisitions: - acquisition = acquisitions[(op, channel_name)] + if (op, channel_id) in acquisitions: + acquisition = acquisitions[(op, channel_id)] else: - acquisition = acquisitions[(op, channel_name)] = create_acquisition( - op, - channel_name, - options, - acq_config.threshold, - acq_config.iq_angle, + acquisition = acquisitions[(op, channel_id)] = create_acquisition( + op, channel_id, options, acq_config.threshold, acq_config.iq_angle ) acquisition.keys.append(readout.acquisition.id) @@ -445,9 +442,9 @@ def play( # register DC elements so that all qubits are # sweetspot even when they are not used - for channel in self.channels.values(): - if isinstance(channel.logical_channel, DcChannel): - self.configure_channel(channel, configs) + for id, channel in self.channels.items(): + if isinstance(channel, DcChannel): + self.configure_channel(id, configs) self.configure_channels(configs, sequence.channels) self.register_pulses(configs, sequence) From 74e81ee7b712dce5e6e44b969142f65a035d3b53 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 12:30:42 +0200 Subject: [PATCH 49/79] fix: Prevent pre-commit removing star-import --- src/qibolab/instruments/qm/components/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/components/__init__.py b/src/qibolab/instruments/qm/components/__init__.py index 0138aa43c..cd2dbfd89 100644 --- a/src/qibolab/instruments/qm/components/__init__.py +++ b/src/qibolab/instruments/qm/components/__init__.py @@ -1,5 +1,5 @@ -from .configs import * from . import configs +from .configs import * # noqa __all__ = [] __all__ += configs.__all__ From 0476677b476aac97be7aa0c5a009fe0d015c3e38 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 12:31:55 +0200 Subject: [PATCH 50/79] fix: Fix missing import --- src/qibolab/instruments/qm/config/config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/config/config.py b/src/qibolab/instruments/qm/config/config.py index 7ed1e6127..b2df24838 100644 --- a/src/qibolab/instruments/qm/config/config.py +++ b/src/qibolab/instruments/qm/config/config.py @@ -9,7 +9,14 @@ from ..components import OpxOutputConfig, QmAcquisitionConfig from .devices import AnalogOutput, Controller, Octave, OctaveInput, OctaveOutput from .elements import AcquireOctaveElement, DcElement, Element, RfOctaveElement -from .pulses import QmAcquisition, QmPulse, Waveform, operation, waveforms_from_pulse +from .pulses import ( + QmAcquisition, + QmPulse, + Waveform, + integration_weights, + operation, + waveforms_from_pulse, +) __all__ = ["QmConfig"] From 5994a3d5affa9e625b5165b92db0022081ebf75a Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 12:36:05 +0200 Subject: [PATCH 51/79] fix: Prevent even more import removal --- src/qibolab/instruments/qm/components/__init__.py | 2 ++ src/qibolab/instruments/qm/controller.py | 14 +++++++------- src/qibolab/qubits.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/qibolab/instruments/qm/components/__init__.py b/src/qibolab/instruments/qm/components/__init__.py index cd2dbfd89..a3e90c16e 100644 --- a/src/qibolab/instruments/qm/components/__init__.py +++ b/src/qibolab/instruments/qm/components/__init__.py @@ -1,4 +1,6 @@ from . import configs + +# TODO: Fix pycln configurations in pre-commit to preserve the following with no comment from .configs import * # noqa __all__ = [] diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index cdc716508..0b40bf03f 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -1,27 +1,27 @@ -from os import PathLike import shutil import tempfile import warnings from collections import defaultdict from dataclasses import asdict, dataclass +from os import PathLike from pathlib import Path from typing import Optional from pydantic import Field -from qibolab.components.configs import AcquisitionConfig, IqConfig, OscillatorConfig -from qibolab.instruments.qm.components.configs import ( - OpxOutputConfig, - QmAcquisitionConfig, -) from qm import QuantumMachinesManager, SimulationConfig, generate_qua_script from qm.octave import QmOctaveConfig from qm.simulate.credentials import create_credentials from qualang_tools.simulator_tools import create_simulator_controller_connections -from qibolab.components import AcquireChannel, Channel, Config, DcChannel, IqChannel +from qibolab.components import AcquireChannel, Config, DcChannel, IqChannel +from qibolab.components.configs import IqConfig, OscillatorConfig from qibolab.execution_parameters import ExecutionParameters from qibolab.identifier import ChannelId from qibolab.instruments.abstract import Controller +from qibolab.instruments.qm.components.configs import ( + OpxOutputConfig, + QmAcquisitionConfig, +) from qibolab.pulses import Acquisition, Align, Delay, Pulse, Readout from qibolab.sequence import PulseSequence from qibolab.sweeper import ParallelSweepers, Parameter, Sweeper diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 6f658ff7c..433ad3430 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -5,7 +5,7 @@ # TODO: the unused import are there because Qibocal is still importing them from here # since the export scheme will be reviewed, it should be changed at that time, removing # the unused ones from here -from .identifier import ChannelId, TransitionId, QubitId, QubitPairId +from .identifier import ChannelId, QubitId, QubitPairId, TransitionId # noqa from .serialize import Model From eeef41f5a15ee871dd1a19303bb2dbf0c7c9856e Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 14:19:08 +0200 Subject: [PATCH 52/79] build: Set py3.11 in flake to allow testing qua --- flake.lock | 38 ++++++++++++++++++++++++++++++++++++++ flake.nix | 5 +++++ 2 files changed, 43 insertions(+) diff --git a/flake.lock b/flake.lock index d0eb4e76f..86505603c 100644 --- a/flake.lock +++ b/flake.lock @@ -138,6 +138,22 @@ "type": "github" } }, + "flake-compat_3": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -289,6 +305,27 @@ "type": "github" } }, + "nixpkgs-python": { + "inputs": { + "flake-compat": "flake-compat_3", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1722978926, + "narHash": "sha256-sqOOEaKJJSUFBzag/cGeeXV491TrrVFY3DFBs1w20V8=", + "owner": "cachix", + "repo": "nixpkgs-python", + "rev": "7c550bca7e6cf95898e32eb2173efe7ebb447460", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "nixpkgs-python", + "type": "github" + } + }, "nixpkgs-regression": { "locked": { "lastModified": 1643052045, @@ -411,6 +448,7 @@ "devenv": "devenv", "fenix": "fenix", "nixpkgs": "nixpkgs_2", + "nixpkgs-python": "nixpkgs-python", "systems": "systems_3" } }, diff --git a/flake.nix b/flake.nix index 3b65860c4..e7ba41854 100644 --- a/flake.nix +++ b/flake.nix @@ -10,6 +10,10 @@ url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; + nixpkgs-python = { + url = "github:cachix/nixpkgs-python"; + inputs = {nixpkgs.follows = "nixpkgs";}; + }; }; outputs = { @@ -62,6 +66,7 @@ languages.python = { enable = true; libraries = with pkgs; [zlib]; + version = "3.11"; poetry = { enable = true; install = { From 22f56261f58984154d49708f9ff5d4d45ce5fdf6 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 14:40:58 +0200 Subject: [PATCH 53/79] fix: Give up on userdict not to spoil serialization --- src/qibolab/instruments/qm/config/devices.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/qibolab/instruments/qm/config/devices.py b/src/qibolab/instruments/qm/config/devices.py index aa017e083..6210e34cf 100644 --- a/src/qibolab/instruments/qm/config/devices.py +++ b/src/qibolab/instruments/qm/config/devices.py @@ -1,4 +1,3 @@ -from collections import UserDict from dataclasses import dataclass, field from typing import Any, Generic, TypeVar @@ -19,7 +18,7 @@ V = TypeVar("V") -class PortDict(Generic[V], UserDict[str, V]): +class PortDict(Generic[V], dict[str, V]): """Dictionary that automatically converts keys to strings. Used to register input and output ports to controllers and Octaves From 8a9195967151f33fe1f47357810e67999f88f632 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 16:09:02 +0200 Subject: [PATCH 54/79] fix: Split repeated validator/serializer --- src/qibolab/identifier.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/qibolab/identifier.py b/src/qibolab/identifier.py index 4db4a3522..7d713696a 100644 --- a/src/qibolab/identifier.py +++ b/src/qibolab/identifier.py @@ -38,16 +38,24 @@ def __str__(self) -> str: StateId = int """State identifier.""" + +def _split(pair: Union[str, tuple]) -> tuple[str, str]: + if isinstance(pair, str): + a, b = pair.split("-") + return a, b + return pair + + +def _join(pair: tuple[str, str]) -> str: + return f"{pair[0]}-{pair[1]}" + + TransitionId = Annotated[ - tuple[StateId, StateId], - BeforeValidator(lambda p: tuple(p.split("-")) if isinstance(p, str) else p), - PlainSerializer(lambda p: f"{p[0]}-{p[1]}"), + tuple[StateId, StateId], BeforeValidator(_split), PlainSerializer(_join) ] """Identifier for a state transition.""" QubitPairId = Annotated[ - tuple[QubitId, QubitId], - BeforeValidator(lambda p: tuple(p.split("-")) if isinstance(p, str) else p), - PlainSerializer(lambda p: f"{p[0]}-{p[1]}"), + tuple[QubitId, QubitId], BeforeValidator(_split), PlainSerializer(_join) ] """Two-qubit active interaction identifier.""" From 202f0dd41a975ed5def413b5becd98e700bf77fb Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 16:16:13 +0200 Subject: [PATCH 55/79] fix: Define channels independently from the instrument in dummy --- src/qibolab/dummy/platform.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index f226cbadd..99abb757e 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -10,11 +10,10 @@ def create_dummy() -> Platform: """Create a dummy platform using the dummy instrument.""" - # register the instruments - instrument = DummyInstrument(name="dummy", address="0.0.0.0") - pump = DummyLocalOscillator(name="twpa_pump", address="0.0.0.0") + pump_name = "twpa_pump" qubits = {} + channels = {} # attach the channels for q in range(5): drive, drive12, flux, probe, acquisition = ( @@ -24,9 +23,9 @@ def create_dummy() -> Platform: f"qubit_{q}/probe", f"qubit_{q}/acquisition", ) - instrument.channels |= { + channels |= { probe: IqChannel(mixer=None, lo=None), - acquisition: AcquireChannel(twpa_pump=pump.name, probe=probe), + acquisition: AcquireChannel(twpa_pump=pump_name, probe=probe), drive: IqChannel(mixer=None, lo=None), drive12: IqChannel(mixer=None, lo=None), flux: DcChannel(), @@ -42,9 +41,13 @@ def create_dummy() -> Platform: couplers = {} for c in (0, 1, 3, 4): flux = f"coupler_{c}/flux" - instrument.channels |= {flux: DcChannel()} + channels |= {flux: DcChannel()} couplers[c] = Qubit(flux=flux) + # register the instruments + instrument = DummyInstrument(name="dummy", address="0.0.0.0", channels=channels) + pump = DummyLocalOscillator(name=pump_name, address="0.0.0.0") + return Platform.load( path=FOLDER, instruments=[instrument, pump], qubits=qubits, couplers=couplers ) From 78b07cb4c381170453030d23b0e4f9e23f1a2162 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 16:40:42 +0200 Subject: [PATCH 56/79] fix: Replace qcodes dependency with more accurate protocol --- src/qibolab/instruments/oscillator.py | 29 +++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/qibolab/instruments/oscillator.py b/src/qibolab/instruments/oscillator.py index 75c732af3..0e708f4b9 100644 --- a/src/qibolab/instruments/oscillator.py +++ b/src/qibolab/instruments/oscillator.py @@ -1,8 +1,7 @@ from abc import abstractmethod -from typing import Optional +from typing import Any, Optional, Protocol from pydantic import Field -from qcodes.instrument import Instrument as QcodesInstrument from qibolab.instruments.abstract import Instrument, InstrumentSettings @@ -10,6 +9,28 @@ """Number of times to attempt connecting to instrument in case of failure.""" +class Device(Protocol): + """Dummy device that does nothing but follows the QCoDeS interface. + + Used by :class:`qibolab.instruments.dummy.DummyLocalOscillator`. + """ + + def set(self, name: str, value: Any): + """Set device property.""" + + def get(self, name: str) -> Any: + """Get device property.""" + + def on(self): + """Turn device on.""" + + def off(self): + """Turn device on.""" + + def close(self): + """Close connection with device.""" + + class LocalOscillatorSettings(InstrumentSettings): """Local oscillator parameters that are saved in the platform runcard.""" @@ -49,7 +70,7 @@ class LocalOscillator(Instrument): qubits and resonators. They cannot be used to play or sweep pulses. """ - device: Optional[QcodesInstrument] = None + device: Optional[Device] = None settings: Optional[InstrumentSettings] = Field( default_factory=lambda: LocalOscillatorSettings() ) @@ -59,7 +80,7 @@ class LocalOscillator(Instrument): ref_osc_source = _property("ref_osc_source") @abstractmethod - def create(self) -> QcodesInstrument: + def create(self) -> Device: """Create instance of physical device.""" def connect(self): From 8159e01b53713bc00934e866cfc151d3ba6afa8b Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 16:53:31 +0200 Subject: [PATCH 57/79] fix: Make device protocol runtime checkable To make it compatible with Pydantic checks --- src/qibolab/instruments/oscillator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qibolab/instruments/oscillator.py b/src/qibolab/instruments/oscillator.py index 0e708f4b9..e62557797 100644 --- a/src/qibolab/instruments/oscillator.py +++ b/src/qibolab/instruments/oscillator.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Any, Optional, Protocol +from typing import Any, Optional, Protocol, runtime_checkable from pydantic import Field @@ -9,6 +9,7 @@ """Number of times to attempt connecting to instrument in case of failure.""" +@runtime_checkable class Device(Protocol): """Dummy device that does nothing but follows the QCoDeS interface. From ad03128ccc6f035c19cd604e199408e61f505732 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 17:26:01 +0200 Subject: [PATCH 58/79] fix: Separately lift model constraints on instruments They are not really going to be serialized, so it's fine to keep them more lenient, at least for a while, to ease the transition. --- src/qibolab/instruments/abstract.py | 2 +- tests/test_dummy.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/qibolab/instruments/abstract.py b/src/qibolab/instruments/abstract.py index 46ed02ffd..558bd82a5 100644 --- a/src/qibolab/instruments/abstract.py +++ b/src/qibolab/instruments/abstract.py @@ -29,7 +29,7 @@ class Instrument(Model, ABC): address (str): Instrument network address. """ - model_config = ConfigDict(frozen=False) + model_config = ConfigDict(arbitrary_types_allowed=True, frozen=False, extra="allow") name: InstrumentId address: str diff --git a/tests/test_dummy.py b/tests/test_dummy.py index c8db6f26e..d37e68636 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -58,7 +58,6 @@ def test_dummy_execute_pulse_sequence_unrolling( ): nshots = 100 nsequences = 10 - platform.instruments["dummy"].UNROLLING_BATCH_SIZE = batch_size natives = platform.natives sequences = [] sequence = PulseSequence() From be7e60392027f31f8d72dc33647bd7e3478bede2 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 26 Aug 2024 18:21:27 +0200 Subject: [PATCH 59/79] docs: Fix doctests for channels outside qubits --- doc/source/tutorials/lab.rst | 55 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/doc/source/tutorials/lab.rst b/doc/source/tutorials/lab.rst index d1bb66cc3..c9d140c26 100644 --- a/doc/source/tutorials/lab.rst +++ b/doc/source/tutorials/lab.rst @@ -120,34 +120,28 @@ the native gates, but separately from the single-qubit ones. ) # create the qubit objects - qubit0 = Qubit(name=0) - qubit1 = Qubit(name=1) + qubit0 = Qubit(drive="0/drive", flux="0/flux", probe="0/probe", acquisition="0/acquisition") + qubit1 = Qubit(drive="1/drive", flux="1/flux", probe="1/probe", acquisition="1/acquisition") + + channels = {} # assign channels to the qubits - qubit0.probe = IqChannel( - name="0/probe", mixer=None, lo=None, acquisition="0/acquisition" - ) - qubit0.acquisition = AcquireChannel( - name="0/acquisition", twpa_pump=None, probe="probe_0" - ) - qubit0.drive = IqChannel(name="0/drive", mixer=None, lo=None) - qubit0.flux = DcChannel(name="0/flux") - qubit1.probe = IqChannel( - name="1/probe", mixer=None, lo=None, acquisition="1/acquisition" - ) - qubit1.acquisition = AcquireChannel( - name="1/acquisition", twpa_pump=None, probe="probe_1" - ) - qubit1.drive = IqChannel(name="1/drive", mixer=None, lo=None) + channels[qubit0.probe] = IqChannel(mixer=None, lo=None) + channels[qubit0.acquisition] = AcquireChannel( twpa_pump=None, probe=qubit0.probe) + channels[qubit0.drive] = IqChannel(mixer=None, lo=None) + channels[qubit0.flux] = DcChannel() + channels[qubit1.probe] = IqChannel( mixer=None, lo=None) + channels[qubit1.acquisition] = AcquireChannel( twpa_pump=None, probe=qubit1.probe) + channels[qubit1.drive] = IqChannel(mixer=None, lo=None) # assign single-qubit native gates to each qubit single_qubit = {} - single_qubit[qubit0.name] = SingleQubitNatives( + single_qubit["0"] = SingleQubitNatives( RX=RxyFactory( PulseSequence( [ ( - qubit0.drive.name, + qubit0.drive, Pulse( duration=40, amplitude=0.05, @@ -161,19 +155,19 @@ the native gates, but separately from the single-qubit ones. PulseSequence( [ ( - qubit0.probe.name, + qubit0.probe, Pulse(duration=1000, amplitude=0.005, envelope=Rectangular()), ) ] ) ), ) - single_qubit[qubit1.name] = SingleQubitNatives( + single_qubit["1"] = SingleQubitNatives( RX=RxyFactory( PulseSequence( [ ( - qubit1.drive.name, + qubit1.drive, Pulse( duration=40, amplitude=0.05, envelope=Gaussian(rel_sigma=0.2) ), @@ -185,7 +179,7 @@ the native gates, but separately from the single-qubit ones. PulseSequence( [ ( - qubit1.probe.name, + qubit1.probe, Pulse(duration=1000, amplitude=0.005, envelope=Rectangular()), ) ] @@ -196,12 +190,12 @@ the native gates, but separately from the single-qubit ones. # define the pair of qubits two_qubit = TwoQubitContainer( { - f"{qubit0.name}-{qubit1.name}": TwoQubitNatives( + f"0-1": TwoQubitNatives( CZ=FixedSequenceFactory( PulseSequence( [ ( - qubit0.flux.name, + qubit0.flux, Pulse(duration=30, amplitude=0.005, envelope=Rectangular()), ), ] @@ -229,12 +223,11 @@ will take them into account when calling :class:`qibolab.native.TwoQubitNatives` ) # create the qubit and coupler objects - qubit0 = Qubit(name=0) - qubit1 = Qubit(name=1) - coupler_01 = Qubit(name="c01") + coupler_01 = Qubit(flux="c01/flux") + channels = {} # assign channel(s) to the coupler - coupler_01.flux = DcChannel(name="c01/flux") + channels[coupler_01.flux] = DcChannel() # assign single-qubit native gates to each qubit # Look above example @@ -242,12 +235,12 @@ will take them into account when calling :class:`qibolab.native.TwoQubitNatives` # define the pair of qubits two_qubit = TwoQubitContainer( { - f"{qubit0.name}-{qubit1.name}": TwoQubitNatives( + f"0-1": TwoQubitNatives( CZ=FixedSequenceFactory( PulseSequence( [ ( - coupler_01.flux.name, + coupler_01.flux, Pulse(duration=30, amplitude=0.005, envelope=Rectangular()), ) ], From 64311a602daee705934068e5c9c127bc0a72e5d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:21:42 +0000 Subject: [PATCH 60/79] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/source/tutorials/lab.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/source/tutorials/lab.rst b/doc/source/tutorials/lab.rst index c9d140c26..c3d23c292 100644 --- a/doc/source/tutorials/lab.rst +++ b/doc/source/tutorials/lab.rst @@ -120,18 +120,22 @@ the native gates, but separately from the single-qubit ones. ) # create the qubit objects - qubit0 = Qubit(drive="0/drive", flux="0/flux", probe="0/probe", acquisition="0/acquisition") - qubit1 = Qubit(drive="1/drive", flux="1/flux", probe="1/probe", acquisition="1/acquisition") + qubit0 = Qubit( + drive="0/drive", flux="0/flux", probe="0/probe", acquisition="0/acquisition" + ) + qubit1 = Qubit( + drive="1/drive", flux="1/flux", probe="1/probe", acquisition="1/acquisition" + ) channels = {} # assign channels to the qubits channels[qubit0.probe] = IqChannel(mixer=None, lo=None) - channels[qubit0.acquisition] = AcquireChannel( twpa_pump=None, probe=qubit0.probe) + channels[qubit0.acquisition] = AcquireChannel(twpa_pump=None, probe=qubit0.probe) channels[qubit0.drive] = IqChannel(mixer=None, lo=None) channels[qubit0.flux] = DcChannel() - channels[qubit1.probe] = IqChannel( mixer=None, lo=None) - channels[qubit1.acquisition] = AcquireChannel( twpa_pump=None, probe=qubit1.probe) + channels[qubit1.probe] = IqChannel(mixer=None, lo=None) + channels[qubit1.acquisition] = AcquireChannel(twpa_pump=None, probe=qubit1.probe) channels[qubit1.drive] = IqChannel(mixer=None, lo=None) # assign single-qubit native gates to each qubit From 4b1f25adb49f8a965212eeb17d790be87c6527a2 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Tue, 27 Aug 2024 13:57:35 +0200 Subject: [PATCH 61/79] fix: Update config kidns extension type --- src/qibolab/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibolab/parameters.py b/src/qibolab/parameters.py index d9ed9c816..4a0af0848 100644 --- a/src/qibolab/parameters.py +++ b/src/qibolab/parameters.py @@ -126,7 +126,7 @@ class ConfigKinds: _registered: list[_ChannelConfigT] = list(_BUILTIN_CONFIGS) @classmethod - def extend(cls, kinds: Iterable[type[Config]]): + def extend(cls, kinds: Iterable[_ChannelConfigT]): """Extend the known configuration kinds. Nested unions are supported (i.e. :class:`Union` as elements of ``kinds``). From a795eab42aa2653a663a015cf176d1f69060836d Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Tue, 27 Aug 2024 15:37:56 +0200 Subject: [PATCH 62/79] fix: Process readout events during qua script creation --- src/qibolab/instruments/qm/program/instructions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index 3fc11289f..934b86dd7 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -7,7 +7,7 @@ from qibolab.components import Config from qibolab.execution_parameters import AcquisitionType, ExecutionParameters from qibolab.identifier import ChannelType -from qibolab.pulses import Align, Delay, Pulse, VirtualZ +from qibolab.pulses import Align, Delay, Pulse, Readout, VirtualZ from qibolab.sweeper import ParallelSweepers, Sweeper from ..config import operation @@ -96,6 +96,8 @@ def play(args: ExecutionArguments): if isinstance(pulse, Delay): _delay(pulse, element, params) elif isinstance(pulse, Pulse): + _play(op, element, params) + elif isinstance(pulse, Readout): acquisition = args.acquisitions.get((op, element)) _play(op, element, params, acquisition) elif isinstance(pulse, VirtualZ): From 1949faaa3c438874137e98e5a603283ccee7b085 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Tue, 27 Aug 2024 16:00:09 +0200 Subject: [PATCH 63/79] fix: Register readout event with its own hash Instead of the associated probe pulse --- src/qibolab/instruments/qm/config/config.py | 7 ++++--- src/qibolab/instruments/qm/controller.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/qibolab/instruments/qm/config/config.py b/src/qibolab/instruments/qm/config/config.py index b2df24838..c5ebb4768 100644 --- a/src/qibolab/instruments/qm/config/config.py +++ b/src/qibolab/instruments/qm/config/config.py @@ -5,6 +5,7 @@ from qibolab.components.configs import IqConfig, OscillatorConfig from qibolab.identifier import ChannelId from qibolab.pulses import Pulse +from qibolab.pulses.pulse import Readout from ..components import OpxOutputConfig, QmAcquisitionConfig from .devices import AnalogOutput, Controller, Octave, OctaveInput, OctaveOutput @@ -142,12 +143,12 @@ def register_dc_pulse(self, element: str, pulse: Pulse): self.elements[element].operations[op] = op return op - def register_acquisition_pulse(self, element: str, pulse: Pulse): + def register_acquisition_pulse(self, element: str, readout: Readout): """Registers pulse, waveforms and integration weights in QM config.""" - op = operation(pulse) + op = operation(readout) acquisition = f"{op}_{element}" if acquisition not in self.pulses: - self.pulses[acquisition] = self.register_waveforms(pulse, element) + self.pulses[acquisition] = self.register_waveforms(readout.probe, element) self.elements[element].operations[op] = acquisition return op diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 0b40bf03f..798d17cfa 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -374,7 +374,7 @@ def register_acquisitions( "Quantum Machines does not support acquisition with different duration than probe." ) - op = self.config.register_acquisition_pulse(channel_id, readout.probe) + op = self.config.register_acquisition_pulse(channel_id, readout) acq_config = configs[channel_id] assert isinstance(acq_config, QmAcquisitionConfig) From fc4b66e81fc6dcad8053a7c6e9f81225b24f5012 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Wed, 28 Aug 2024 16:53:25 +0200 Subject: [PATCH 64/79] test: Properly move unrolling test Slightly messed up during rebase --- tests/test_platform.py | 16 ---------------- tests/test_unrolling.py | 5 ++++- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/tests/test_platform.py b/tests/test_platform.py index c300796e6..9c9f0969b 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -30,22 +30,6 @@ nshots = 1024 -def test_unroll_sequences(platform: Platform): - qubit = next(iter(platform.qubits.values())) - assert qubit.probe is not None - natives = platform.natives.single_qubit[0] - assert natives.RX is not None - assert natives.MZ is not None - sequence = PulseSequence() - sequence.concatenate(natives.RX.create_sequence()) - sequence.append((qubit.probe, Delay(duration=sequence.duration))) - sequence.concatenate(natives.MZ.create_sequence()) - total_sequence, readouts = unroll_sequences(10 * [sequence], relaxation_time=10000) - assert len(total_sequence.acquisitions) == 10 - assert len(readouts) == 1 - assert all(len(readouts[acq.id]) == 10 for _, acq in sequence.acquisitions) - - def test_create_platform(platform): assert isinstance(platform, Platform) diff --git a/tests/test_unrolling.py b/tests/test_unrolling.py index a87e3d7d8..2d612a949 100644 --- a/tests/test_unrolling.py +++ b/tests/test_unrolling.py @@ -109,10 +109,13 @@ def test_batch(bounds): def test_unroll_sequences(platform: Platform): qubit = next(iter(platform.qubits.values())) + assert qubit.probe is not None natives = platform.natives.single_qubit[0] + assert natives.RX is not None + assert natives.MZ is not None sequence = PulseSequence() sequence.concatenate(natives.RX.create_sequence()) - sequence.append((qubit.probe.name, Delay(duration=sequence.duration))) + sequence.append((qubit.probe, Delay(duration=sequence.duration))) sequence.concatenate(natives.MZ.create_sequence()) total_sequence, readouts = unroll_sequences(10 * [sequence], relaxation_time=10000) assert len(total_sequence.acquisitions) == 10 From 6fad60dce3a329a5dc4cdbd78561baca081b04c6 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Wed, 28 Aug 2024 17:51:33 +0200 Subject: [PATCH 65/79] test: Update test to Pydantic instruments --- tests/test_instruments_bluefors.py | 6 +++--- tests/test_instruments_oscillator.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_instruments_bluefors.py b/tests/test_instruments_bluefors.py index 84975b0cd..af1541bba 100644 --- a/tests/test_instruments_bluefors.py +++ b/tests/test_instruments_bluefors.py @@ -14,7 +14,7 @@ def test_connect(): with mock.patch("socket.socket"): - tc = TemperatureController("Test_Temperature_Controller", "") + tc = TemperatureController(name="Test_Temperature_Controller", address="") assert tc.is_connected is False # if already connected, it should stay connected for _ in range(2): @@ -25,7 +25,7 @@ def test_connect(): @pytest.mark.parametrize("already_connected", [True, False]) def test_disconnect(already_connected): with mock.patch("socket.socket"): - tc = TemperatureController("Test_Temperature_Controller", "") + tc = TemperatureController(name="Test_Temperature_Controller", address="") if not already_connected: tc.connect() # if already disconnected, it should stay disconnected @@ -39,7 +39,7 @@ def test_continuously_read_data(): "qibolab.instruments.bluefors.TemperatureController.get_data", new=lambda _: yaml.safe_load(messages[0]), ): - tc = TemperatureController("Test_Temperature_Controller", "") + tc = TemperatureController(name="Test_Temperature_Controller", address="") read_temperatures = tc.read_data() for read_temperature in read_temperatures: assert read_temperature == yaml.safe_load(messages[0]) diff --git a/tests/test_instruments_oscillator.py b/tests/test_instruments_oscillator.py index 56a92deac..166892b0d 100644 --- a/tests/test_instruments_oscillator.py +++ b/tests/test_instruments_oscillator.py @@ -5,7 +5,7 @@ @pytest.fixture def lo(): - return DummyLocalOscillator("lo", "0") + return DummyLocalOscillator(name="lo", address="0") def test_oscillator_init(lo): From a47877743fab186daeccf27b371e509a82f9060b Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Wed, 28 Aug 2024 17:56:19 +0200 Subject: [PATCH 66/79] test: Move instruments' tests in a single scope --- tests/conftest.py | 20 ------------------ tests/instruments/__init__.py | 0 tests/instruments/conftest.py | 21 +++++++++++++++++++ .../test_bluefors.py} | 0 .../test_erasynth.py} | 0 .../test_oscillator.py} | 0 .../test_rohde_schwarz.py} | 0 7 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 tests/instruments/__init__.py create mode 100644 tests/instruments/conftest.py rename tests/{test_instruments_bluefors.py => instruments/test_bluefors.py} (100%) rename tests/{test_instruments_erasynth.py => instruments/test_erasynth.py} (100%) rename tests/{test_instruments_oscillator.py => instruments/test_oscillator.py} (100%) rename tests/{test_instruments_rohde_schwarz.py => instruments/test_rohde_schwarz.py} (100%) diff --git a/tests/conftest.py b/tests/conftest.py index de5ba56cd..ddcdf7726 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,26 +69,6 @@ def emulators(): os.environ[PLATFORMS] = str(pathlib.Path(__file__).parent / "emulators") -def find_instrument(platform, instrument_type): - for instrument in platform.instruments.values(): - if isinstance(instrument, instrument_type): - return instrument - return None - - -def get_instrument(platform, instrument_type): - """Finds if an instrument of a given type exists in the given platform. - - If the platform does not have such an instrument, the corresponding - test that asked for this instrument is skipped. This ensures that - QPU tests are executed only on the available instruments. - """ - instrument = find_instrument(platform, instrument_type) - if instrument is None: - pytest.skip(f"Skipping {instrument_type.__name__} test for {platform.name}.") - return instrument - - @pytest.fixture(scope="module", params=TESTING_PLATFORM_NAMES) def platform(request): """Dummy platform to be used when there is no access to QPU. diff --git a/tests/instruments/__init__.py b/tests/instruments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/instruments/conftest.py b/tests/instruments/conftest.py new file mode 100644 index 000000000..a99739028 --- /dev/null +++ b/tests/instruments/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +def find_instrument(platform, instrument_type): + for instrument in platform.instruments.values(): + if isinstance(instrument, instrument_type): + return instrument + return None + + +def get_instrument(platform, instrument_type): + """Finds if an instrument of a given type exists in the given platform. + + If the platform does not have such an instrument, the corresponding + test that asked for this instrument is skipped. This ensures that + QPU tests are executed only on the available instruments. + """ + instrument = find_instrument(platform, instrument_type) + if instrument is None: + pytest.skip(f"Skipping {instrument_type.__name__} test for {platform.name}.") + return instrument diff --git a/tests/test_instruments_bluefors.py b/tests/instruments/test_bluefors.py similarity index 100% rename from tests/test_instruments_bluefors.py rename to tests/instruments/test_bluefors.py diff --git a/tests/test_instruments_erasynth.py b/tests/instruments/test_erasynth.py similarity index 100% rename from tests/test_instruments_erasynth.py rename to tests/instruments/test_erasynth.py diff --git a/tests/test_instruments_oscillator.py b/tests/instruments/test_oscillator.py similarity index 100% rename from tests/test_instruments_oscillator.py rename to tests/instruments/test_oscillator.py diff --git a/tests/test_instruments_rohde_schwarz.py b/tests/instruments/test_rohde_schwarz.py similarity index 100% rename from tests/test_instruments_rohde_schwarz.py rename to tests/instruments/test_rohde_schwarz.py From 25f8b8a56f86d4f0853960345a5667e76f19904b Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Wed, 28 Aug 2024 18:25:10 +0200 Subject: [PATCH 67/79] docs: Fix channel id usage after rebase --- doc/source/getting-started/experiment.rst | 2 +- doc/source/tutorials/calibration.rst | 4 ++-- doc/source/tutorials/lab.rst | 8 +++---- tests/test_platform.py | 26 +++++++++++------------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/source/getting-started/experiment.rst b/doc/source/getting-started/experiment.rst index b9621107d..c6a895b55 100644 --- a/doc/source/getting-started/experiment.rst +++ b/doc/source/getting-started/experiment.rst @@ -221,7 +221,7 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her sequence = natives.MZ.create_sequence() # define a sweeper for a frequency scan - f0 = platform.config(str(qubit.probe.name)).frequency # center frequency + f0 = platform.config(qubit.probe).frequency # center frequency sweeper = Sweeper( parameter=Parameter.frequency, range=(f0 - 2e8, f0 + 2e8, 1e6), diff --git a/doc/source/tutorials/calibration.rst b/doc/source/tutorials/calibration.rst index d55defa4d..c3d5a2a19 100644 --- a/doc/source/tutorials/calibration.rst +++ b/doc/source/tutorials/calibration.rst @@ -47,7 +47,7 @@ around the pre-defined frequency. sequence = natives.MZ.create_sequence() # allocate frequency sweeper - f0 = platform.config(str(qubit.probe.name)).frequency + f0 = platform.config(qubit.probe).frequency sweeper = Sweeper( parameter=Parameter.frequency, range=(f0 - 2e8, f0 + 2e8, 1e6), @@ -141,7 +141,7 @@ complex pulse sequence. Therefore with start with that: sequence.concatenate(natives.MZ.create_sequence()) # allocate frequency sweeper - f0 = platform.config(str(qubit.probe.name)).frequency + f0 = platform.config(qubit.probe).frequency sweeper = Sweeper( parameter=Parameter.frequency, range=(f0 - 2e8, f0 + 2e8, 1e6), diff --git a/doc/source/tutorials/lab.rst b/doc/source/tutorials/lab.rst index c3d23c292..5a16bfb48 100644 --- a/doc/source/tutorials/lab.rst +++ b/doc/source/tutorials/lab.rst @@ -48,14 +48,14 @@ using different Qibolab primitives. # define configuration for channels configs = {} - configs[qubit.drive.name] = IqConfig(frequency=3e9) - configs[qubit.probe.name] = IqConfig(frequency=7e9) + configs[qubit.drive] = IqConfig(frequency=3e9) + configs[qubit.probe] = IqConfig(frequency=7e9) # create sequence that drives qubit from state 0 to 1 drive_seq = PulseSequence( [ ( - qubit.drive.name, + qubit.drive, Pulse(duration=40, amplitude=0.05, envelope=Gaussian(rel_sigma=0.2)), ) ] @@ -65,7 +65,7 @@ using different Qibolab primitives. probe_seq = PulseSequence( [ ( - qubit.probe.name, + qubit.probe, Pulse(duration=1000, amplitude=0.005, envelope=Rectangular()), ) ] diff --git a/tests/test_platform.py b/tests/test_platform.py index 9c9f0969b..d89bd0994 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -227,7 +227,7 @@ def test_platform_execute_one_drive_pulse(qpu_platform): sequence = PulseSequence( [ ( - qubit.drive.name, + qubit.drive, Pulse(duration=200, amplitude=0.07, envelope=Gaussian(0.2)), ) ] @@ -248,7 +248,7 @@ def test_platform_execute_one_coupler_pulse(qpu_platform): sequence = PulseSequence( [ ( - coupler.flux.name, + coupler.flux, Pulse(duration=200, amplitude=0.31, envelope=Rectangular()), ) ] @@ -267,7 +267,7 @@ def test_platform_execute_one_flux_pulse(qpu_platform): sequence = PulseSequence( [ ( - qubit.flux.name, + qubit.flux, Pulse(duration=200, amplitude=0.28, envelope=Rectangular()), ) ] @@ -307,7 +307,7 @@ def test_platform_execute_one_drive_one_readout(qpu_platform): qubit_id, qubit = next(iter(platform.qubits.items())) sequence = PulseSequence() sequence.concatenate(platform.create_RX_pulse(qubit_id)) - sequence.append((qubit.probe.name, Delay(duration=200))) + sequence.append((qubit.probe, Delay(duration=200))) sequence.concatenate(platform.create_MZ_pulse(qubit_id)) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -323,7 +323,7 @@ def test_platform_execute_multiple_drive_pulses_one_readout(qpu_platform): sequence.concatenate(platform.create_RX_pulse(qubit_id)) sequence.append((qubit.drive.name, Delay(duration=4))) sequence.concatenate(platform.create_RX_pulse(qubit_id)) - sequence.append((qubit.probe.name, Delay(duration=808))) + sequence.append((qubit.probe, Delay(duration=808))) sequence.concatenate(platform.create_MZ_pulse(qubit_id)) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -340,7 +340,7 @@ def test_platform_execute_multiple_drive_pulses_one_readout_no_spacing( sequence.concatenate(platform.create_RX_pulse(qubit_id)) sequence.concatenate(platform.create_RX_pulse(qubit_id)) sequence.concatenate(platform.create_RX_pulse(qubit_id)) - sequence.append((qubit.probe.name, Delay(duration=800))) + sequence.append((qubit.probe, Delay(duration=800))) sequence.concatenate(platform.create_MZ_pulse(qubit_id)) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -355,9 +355,9 @@ def test_platform_execute_multiple_overlaping_drive_pulses_one_readout( pulse = Pulse(duration=200, amplitude=0.08, envelope=Gaussian(rel_sigma=1 / 7)) sequence = PulseSequence( [ - (qubit.drive.name, pulse), - (qubit.drive12.name, pulse.model_copy()), - (qubit.probe.name, Delay(duration=800)), + (qubit.drive, pulse), + (qubit.drive12, pulse.model_copy()), + (qubit.probe, Delay(duration=800)), ] ) sequence.concatenate(platform.create_MZ_pulse(qubit_id)) @@ -375,11 +375,11 @@ def test_platform_execute_multiple_readout_pulses(qpu_platform): qd_seq2 = platform.create_RX_pulse(qubit_id) ro_seq2 = platform.create_MZ_pulse(qubit_id) sequence.concatenate(qd_seq1) - sequence.append((qubit.probe.name, Delay(duration=qd_seq1.duration))) + sequence.append((qubit.probe, Delay(duration=qd_seq1.duration))) sequence.concatenate(ro_seq1) sequence.append((qubit.drive.name, Delay(duration=ro_seq1.duration))) sequence.concatenate(qd_seq2) - sequence.append((qubit.probe.name, Delay(duration=qd_seq2.duration))) + sequence.append((qubit.probe, Delay(duration=qd_seq2.duration))) sequence.concatenate(ro_seq2) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -395,7 +395,7 @@ def test_excited_state_probabilities_pulses(qpu_platform): sequence = PulseSequence() for qubit_id, qubit in platform.qubits.items(): sequence.concatenate(platform.create_RX_pulse(qubit_id)) - sequence.append((qubit.probe.name, Delay(duration=sequence.duration))) + sequence.append((qubit.probe, Delay(duration=sequence.duration))) sequence.concatenate(platform.create_MZ_pulse(qubit_id)) result = platform.execute([sequence], ExecutionParameters(nshots=5000)) @@ -425,7 +425,7 @@ def test_ground_state_probabilities_pulses(qpu_platform, start_zero): if not start_zero: sequence.append( ( - qubit.probe.name, + qubit.probe, Delay( duration=platform.create_RX_pulse(qubit_id).duration, ), From 5ebd2ce5feca071cc790eba14028e47bb266c9d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:52:06 +0000 Subject: [PATCH 68/79] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index ddcdf7726..ec0d541ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ from collections.abc import Callable from typing import Optional -import numpy as np import numpy.typing as npt import pytest From f43f4baabda9c1657378f5bba53d7107d5580ff0 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 30 Aug 2024 17:43:21 +0200 Subject: [PATCH 69/79] test: Update tests for channel ids removal --- tests/test_dummy.py | 8 ++++---- tests/test_sweeper.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_dummy.py b/tests/test_dummy.py index d37e68636..5ebf96c85 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -21,13 +21,13 @@ def test_dummy_initialization(platform: Platform): def test_dummy_execute_coupler_pulse(platform: Platform): sequence = PulseSequence() - channel = platform.get_coupler(0).flux + channel = platform.coupler(0)[1].flux pulse = Pulse( duration=30, amplitude=0.05, envelope=GaussianSquare(rel_sigma=5, width=0.75), ) - sequence.append((channel.name, pulse)) + sequence.append((channel, pulse)) options = ExecutionParameters(nshots=None) _ = platform.execute([sequence], options) @@ -41,8 +41,8 @@ def test_dummy_execute_pulse_sequence_couplers(): cz = natives.two_qubit[(1, 2)].CZ.create_sequence() sequence.concatenate(cz) - sequence.append((platform.qubits[0].probe.name, Delay(duration=40))) - sequence.append((platform.qubits[2].probe.name, Delay(duration=40))) + sequence.append((platform.qubits[0].probe, Delay(duration=40))) + sequence.append((platform.qubits[2].probe, Delay(duration=40))) sequence.concatenate(natives.single_qubit[0].MZ.create_sequence()) sequence.concatenate(natives.single_qubit[2].MZ.create_sequence()) options = ExecutionParameters(nshots=None) diff --git a/tests/test_sweeper.py b/tests/test_sweeper.py index ff02cd245..e673960f8 100644 --- a/tests/test_sweeper.py +++ b/tests/test_sweeper.py @@ -27,7 +27,7 @@ def test_sweeper_pulses(parameter): @pytest.mark.parametrize("parameter", Parameter) def test_sweeper_channels(parameter): - channel = ChannelId.load("0/probe") + channel = "0/probe" parameter_range = np.random.randint(10, size=10) if parameter in Parameter.channels(): sweeper = Sweeper( @@ -40,7 +40,7 @@ def test_sweeper_channels(parameter): def test_sweeper_errors(): - channel = ChannelId.load("0/probe") + channel = "0/probe" pulse = Pulse( duration=40, amplitude=0.1, From e953ace7fa4232116948fb6ae7c72e580e181b36 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 30 Aug 2024 17:43:55 +0200 Subject: [PATCH 70/79] docs: Update docs for channel ids removal --- doc/source/main-documentation/qibolab.rst | 4 ++-- tests/test_sweeper.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/source/main-documentation/qibolab.rst b/doc/source/main-documentation/qibolab.rst index 6ccc4bee9..9f3cb84c5 100644 --- a/doc/source/main-documentation/qibolab.rst +++ b/doc/source/main-documentation/qibolab.rst @@ -433,7 +433,7 @@ For example: sequence.append((qubit.probe, Delay(duration=sequence.duration))) sequence.concatenate(natives.MZ.create_sequence()) - f0 = platform.config(str(qubit.drive.name)).frequency + f0 = platform.config(str(qubit.drive)).frequency sweeper_freq = Sweeper( parameter=Parameter.frequency, range=(f0 - 100_000, f0 + 100_000, 10_000), @@ -551,7 +551,7 @@ The shape of the values of an integreted acquisition with 2 sweepers will be: .. testcode:: python - f0 = platform.config(str(qubit.drive.name)).frequency + f0 = platform.config(str(qubit.drive)).frequency sweeper1 = Sweeper( parameter=Parameter.frequency, range=(f0 - 100_000, f0 + 100_000, 1), diff --git a/tests/test_sweeper.py b/tests/test_sweeper.py index e673960f8..7b830604c 100644 --- a/tests/test_sweeper.py +++ b/tests/test_sweeper.py @@ -1,7 +1,6 @@ import numpy as np import pytest -from qibolab.identifier import ChannelId from qibolab.pulses import Pulse, Rectangular from qibolab.sweeper import Parameter, Sweeper From ab6ad2e6865a33ea49df0f0640a1a687288da527 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 30 Aug 2024 18:38:25 +0200 Subject: [PATCH 71/79] fix: Process acquisition channels together with the others --- src/qibolab/instruments/qm/program/instructions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index 934b86dd7..fc4eafbca 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -6,7 +6,6 @@ from qibolab.components import Config from qibolab.execution_parameters import AcquisitionType, ExecutionParameters -from qibolab.identifier import ChannelType from qibolab.pulses import Align, Delay, Pulse, Readout, VirtualZ from qibolab.sweeper import ParallelSweepers, Sweeper @@ -87,9 +86,6 @@ def play(args: ExecutionArguments): processed_aligns = set() for channel_id, pulse in args.sequence: - if channel_id.channel_type is ChannelType.ACQUISITION: - continue - element = str(channel_id) op = operation(pulse) params = args.parameters[op] From 92b8638c9140082f9a223564927bda2206af3387 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 30 Aug 2024 19:04:26 +0200 Subject: [PATCH 72/79] fix: Register probe pulses within readouts in qm --- src/qibolab/instruments/qm/controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 798d17cfa..a2691f190 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -312,6 +312,8 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): if isinstance(pulse, Pulse): self.register_pulse(id, pulse) + elif isinstance(pulse, Readout): + self.register_pulse(id, pulse.probe) def register_duration_sweeper_pulses( self, args: ExecutionArguments, sweeper: Sweeper From bce178afeda876f5490aa5360d0909aaca5d0cc4 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Fri, 30 Aug 2024 19:14:08 +0200 Subject: [PATCH 73/79] fix: Fix event registered Make type check useful, to catch this error later on --- src/qibolab/instruments/qm/controller.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index a2691f190..894370f6e 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -5,7 +5,7 @@ from dataclasses import asdict, dataclass from os import PathLike from pathlib import Path -from typing import Optional +from typing import Optional, Union from pydantic import Field from qm import QuantumMachinesManager, SimulationConfig, generate_qua_script @@ -284,16 +284,19 @@ def configure_channels(self, configs: dict[str, Config], channels: set[ChannelId for id in channels: self.configure_channel(id, configs) - def register_pulse(self, channel: ChannelId, pulse: Pulse) -> str: + def register_pulse(self, channel: ChannelId, pulse: Union[Pulse, Readout]) -> str: """Add pulse in the QM ``config``. And return corresponding operation. """ ch = self.channels[channel] if isinstance(ch, DcChannel): + assert isinstance(pulse, Pulse) return self.config.register_dc_pulse(channel, pulse) if isinstance(ch, IqChannel): + assert isinstance(pulse, Pulse) return self.config.register_iq_pulse(channel, pulse) + assert isinstance(pulse, Readout) return self.config.register_acquisition_pulse(channel, pulse) def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): @@ -313,7 +316,7 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): if isinstance(pulse, Pulse): self.register_pulse(id, pulse) elif isinstance(pulse, Readout): - self.register_pulse(id, pulse.probe) + self.register_pulse(id, pulse) def register_duration_sweeper_pulses( self, args: ExecutionArguments, sweeper: Sweeper From d5b7c6f339ee2259923c23ad618ad5c260bf6ba5 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 1 Sep 2024 02:54:07 +0400 Subject: [PATCH 74/79] fix: duration and amplitude sweeper register methods --- src/qibolab/instruments/qm/controller.py | 11 ++++------- src/qibolab/instruments/qm/program/sweepers.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 894370f6e..88b4e3cd4 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -330,14 +330,13 @@ def register_duration_sweeper_pulses( continue params = args.parameters[operation(pulse)] - channel_ids = args.sequence.pulse_channels(pulse.id) - channel = self.channels[channel_ids[0]].logical_channel + ids = args.sequence.pulse_channels(pulse.id) original_pulse = ( pulse if params.amplitude_pulse is None else params.amplitude_pulse ) for value in sweeper.values: sweep_pulse = original_pulse.model_copy(update={"duration": value}) - sweep_op = self.register_pulse(channel, sweep_pulse) + sweep_op = self.register_pulse(ids[0], sweep_pulse) params.duration_ops.append((value, sweep_op)) def register_amplitude_sweeper_pulses( @@ -350,13 +349,11 @@ def register_amplitude_sweeper_pulses( """ amplitude = sweeper_amplitude(sweeper.values) for pulse in sweeper.pulses: - channel_ids = args.sequence.pulse_channels(pulse.id) - channel = self.channels[channel_ids[0]].logical_channel sweep_pulse = pulse.model_copy(update={"amplitude": amplitude}) - + ids = args.sequence.pulse_channels(pulse.id) params = args.parameters[operation(pulse)] params.amplitude_pulse = sweep_pulse - params.amplitude_op = self.register_pulse(channel, sweep_pulse) + params.amplitude_op = self.register_pulse(ids[0], sweep_pulse) def register_acquisitions( self, diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 7be03a4af..07a66f187 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -60,7 +60,7 @@ def normalize_phase(values: npt.NDArray) -> npt.NDArray: def normalize_duration(values: npt.NDArray) -> npt.NDArray: """Convert duration from ns to clock cycles (clock cycle = 4ns).""" - if any(values < 16) and not all(values % 4 == 0): + if any(values < 16) or not all(values % 4 == 0): raise ValueError( "Cannot use interpolated duration sweeper for durations that are not multiple of 4ns or are less than 16ns. Please use normal duration sweeper." ) From 035f875b4734c444699c6686ebf328df7b96efaf Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 2 Sep 2024 18:50:07 +0200 Subject: [PATCH 75/79] docs: Remove unrequired f-strings Co-authored-by: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> --- doc/source/tutorials/lab.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorials/lab.rst b/doc/source/tutorials/lab.rst index 5a16bfb48..458299a8e 100644 --- a/doc/source/tutorials/lab.rst +++ b/doc/source/tutorials/lab.rst @@ -194,7 +194,7 @@ the native gates, but separately from the single-qubit ones. # define the pair of qubits two_qubit = TwoQubitContainer( { - f"0-1": TwoQubitNatives( + "0-1": TwoQubitNatives( CZ=FixedSequenceFactory( PulseSequence( [ @@ -239,7 +239,7 @@ will take them into account when calling :class:`qibolab.native.TwoQubitNatives` # define the pair of qubits two_qubit = TwoQubitContainer( { - f"0-1": TwoQubitNatives( + "0-1": TwoQubitNatives( CZ=FixedSequenceFactory( PulseSequence( [ From 3fba10e1ad980ef5a9ded100552bb600b0ff9b92 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 2 Sep 2024 18:51:59 +0200 Subject: [PATCH 76/79] docs: Remove one mention to kernels.npz As they are now embedded in the parameters.json --- doc/source/getting-started/experiment.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/source/getting-started/experiment.rst b/doc/source/getting-started/experiment.rst index c6a895b55..e0322d8b5 100644 --- a/doc/source/getting-started/experiment.rst +++ b/doc/source/getting-started/experiment.rst @@ -12,11 +12,9 @@ To define a platform the user needs to provide a folder with the following struc my_platform/ platform.py parameters.json - kernels.npz # (optional) where ``platform.py`` contains instruments information, ``parameters.json`` -includes calibration parameters and ``kernels.npz`` is an optional -file with additional calibration parameters. +includes calibration parameters. More information about defining platforms is provided in :doc:`../tutorials/lab` and several examples can be found at `TII dedicated repository `_. From 431f3a18f4e7450e66a046f71c5d55ee2f75ba4e Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 2 Sep 2024 18:54:39 +0200 Subject: [PATCH 77/79] docs: Simplify frequencies settings As they are already in the sweepers --- doc/source/getting-started/experiment.rst | 2 +- doc/source/tutorials/calibration.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/getting-started/experiment.rst b/doc/source/getting-started/experiment.rst index e0322d8b5..16d7685a7 100644 --- a/doc/source/getting-started/experiment.rst +++ b/doc/source/getting-started/experiment.rst @@ -239,7 +239,7 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her # plot the results amplitudes = magnitude(results[acq.id][0]) - frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.probe).frequency + frequencies = sweeper.values plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") diff --git a/doc/source/tutorials/calibration.rst b/doc/source/tutorials/calibration.rst index c3d5a2a19..ff5620d7a 100644 --- a/doc/source/tutorials/calibration.rst +++ b/doc/source/tutorials/calibration.rst @@ -75,7 +75,7 @@ In few seconds, the experiment will be finished and we can proceed to plot it. acq = sequence.acquisitions[0][1] amplitudes = magnitude(results[acq.id][0]) - frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.probe).frequency + frequencies = sweeper.values plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") @@ -166,7 +166,7 @@ We can now proceed to launch on hardware: _, acq = next(iter(sequence.acquisitions)) amplitudes = magnitude(results[acq.id][0]) - frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.drive).frequency + frequencies = sweeper.values plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") From 0e9e7228a8dc279187300aea84a7ba576b047850 Mon Sep 17 00:00:00 2001 From: Alessandro Candido Date: Mon, 2 Sep 2024 18:57:08 +0200 Subject: [PATCH 78/79] feat!: Drop channel type --- src/qibolab/identifier.py | 18 ------------------ src/qibolab/sequence.py | 3 +-- tests/test_identifier.py | 5 ----- 3 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 tests/test_identifier.py diff --git a/src/qibolab/identifier.py b/src/qibolab/identifier.py index 7d713696a..ae5204f24 100644 --- a/src/qibolab/identifier.py +++ b/src/qibolab/identifier.py @@ -1,4 +1,3 @@ -from enum import Enum from typing import Annotated, Union from pydantic import BeforeValidator, Field, PlainSerializer @@ -14,23 +13,6 @@ """Type for holding ``QubitPair``s in the ``platform.pairs`` dictionary.""" -# TODO: replace with StrEnum, once py3.10 will be abandoned -# at which point, it will also be possible to replace values with auto() -class ChannelType(str, Enum): - """Names of channels that belong to a qubit. - - Not all channels are required to operate a qubit. - """ - - PROBE = "probe" - ACQUISITION = "acquisition" - DRIVE = "drive" - FLUX = "flux" - - def __str__(self) -> str: - return str(self.value) - - ChannelId = str """Unique identifier for a channel.""" diff --git a/src/qibolab/sequence.py b/src/qibolab/sequence.py index 201b1bb70..48e058bfc 100644 --- a/src/qibolab/sequence.py +++ b/src/qibolab/sequence.py @@ -153,8 +153,7 @@ def acquisitions(self) -> list[tuple[ChannelId, InputOps]]: .. note:: This selects only the :class:`Acquisition` events, and not all the - instructions directed to an acquistion channel (i.e. - :attr:`ChannelType.ACQUISITION`) + instructions directed to an acquistion channel """ # pulse filter needed to exclude delays return [(ch, p) for ch, p in self if isinstance(p, (Acquisition, Readout))] diff --git a/tests/test_identifier.py b/tests/test_identifier.py deleted file mode 100644 index 86dab3340..000000000 --- a/tests/test_identifier.py +++ /dev/null @@ -1,5 +0,0 @@ -from qibolab.identifier import ChannelType - - -def test_channel_type(): - assert str(ChannelType.ACQUISITION) == "acquisition" From 585eb0b92b7e57f61f97c19443fe0b2c326582a3 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 2 Sep 2024 20:41:51 +0300 Subject: [PATCH 79/79] fix: QM frequency and offset sweeper after dropping channel.name --- src/qibolab/instruments/qm/controller.py | 5 ++- .../instruments/qm/program/arguments.py | 7 ++- .../instruments/qm/program/instructions.py | 3 +- .../instruments/qm/program/sweepers.py | 43 ++++++++++--------- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 88b4e3cd4..0c1e2ef5e 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -29,7 +29,7 @@ from .config import SAMPLING_RATE, QmConfig, operation from .program import ExecutionArguments, create_acquisition, program -from .program.sweepers import check_frequency_bandwidth, sweeper_amplitude +from .program.sweepers import find_lo_frequencies, sweeper_amplitude OCTAVE_ADDRESS_OFFSET = 11000 """Offset to be added to Octave addresses, because they must be 11xxx, where @@ -404,7 +404,8 @@ def preprocess_sweeps( Amplitude and duration sweeps require registering additional pulses in the QM ``config. """ for sweeper in find_sweepers(sweepers, Parameter.frequency): - check_frequency_bandwidth(sweeper.channels, configs, sweeper.values) + channels = [(id, self.channels[id]) for id in sweeper.channels] + find_lo_frequencies(args, channels, configs, sweeper.values) for sweeper in find_sweepers(sweepers, Parameter.amplitude): self.register_amplitude_sweeper_pulses(args, sweeper) for sweeper in find_sweepers(sweepers, Parameter.duration): diff --git a/src/qibolab/instruments/qm/program/arguments.py b/src/qibolab/instruments/qm/program/arguments.py index eaf7cd270..dd9b428c8 100644 --- a/src/qibolab/instruments/qm/program/arguments.py +++ b/src/qibolab/instruments/qm/program/arguments.py @@ -1,9 +1,10 @@ from collections import defaultdict from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Union from qm.qua._dsl import _Variable # for type declaration only +from qibolab.identifier import ChannelId from qibolab.pulses import Pulse from qibolab.sequence import PulseSequence @@ -24,6 +25,8 @@ class Parameters: duration_ops: list[tuple[float, str]] = field(default_factory=list) interpolated: bool = False + lo_frequency: Optional[int] = None + @dataclass class ExecutionArguments: @@ -36,6 +39,6 @@ class ExecutionArguments: sequence: PulseSequence acquisitions: Acquisitions relaxation_time: int = 0 - parameters: dict[str, Parameters] = field( + parameters: dict[Union[str, ChannelId], Parameters] = field( default_factory=lambda: defaultdict(Parameters) ) diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index fc4eafbca..953ace4f5 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -151,7 +151,8 @@ def sweep( method(variable, params) else: for channel in sweeper.channels: - method(variable, channel, configs) + params = args.parameters[channel] + method(variable, params, channel) sweep(sweepers[1:], configs, args) diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 07a66f187..2b9aa29f3 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -1,13 +1,13 @@ import numpy as np import numpy.typing as npt -from qibo.config import raise_error from qm import qua from qm.qua._dsl import _Variable # for type declaration only from qibolab.components import Channel, Config +from qibolab.identifier import ChannelId from qibolab.sweeper import Parameter -from .arguments import Parameters +from .arguments import ExecutionArguments, Parameters MAX_OFFSET = 0.5 """Maximum voltage supported by Quantum Machines OPX+ instrument in volts.""" @@ -20,20 +20,26 @@ """Quantum Machines OPX+ frequency bandwidth in Hz.""" -def check_frequency_bandwidth( - channels: list[Channel], configs: dict[str, Channel], values: npt.NDArray +def find_lo_frequencies( + args: ExecutionArguments, + channels: list[tuple[ChannelId, Channel]], + configs: dict[str, Config], + values: npt.NDArray, ): - """Check if frequency sweep is within the supported instrument bandwidth - [-400, 400] MHz.""" - for channel in channels: - name = str(channel.name) + """Register LO frequencies of swept channels in execution arguments. + + These are needed to calculate the proper IF when sweeping frequency. + It also checks if frequency sweep is within the supported instrument + bandwidth [-400, 400] MHz. + """ + for id, channel in channels: lo_frequency = configs[channel.lo].frequency max_freq = max(abs(values - lo_frequency)) if max_freq > FREQUENCY_BANDWIDTH: - raise_error( - ValueError, - f"Frequency {max_freq} for channel {name} is beyond instrument bandwidth.", + raise ValueError( + f"Frequency {max_freq} for channel {id} is beyond instrument bandwidth." ) + args.parameters[id].lo_frequency = int(lo_frequency) def sweeper_amplitude(values: npt.NDArray) -> float: @@ -84,20 +90,17 @@ def _duration_interpolated(variable: _Variable, parameters: Parameters): parameters.interpolated = True -def _offset(variable: _Variable, channel: Channel, configs: dict[str, Config]): - name = str(channel.name) +def _offset(variable: _Variable, parameters: Parameters, element: ChannelId): with qua.if_(variable >= MAX_OFFSET): - qua.set_dc_offset(name, "single", MAX_OFFSET) + qua.set_dc_offset(element, "single", MAX_OFFSET) with qua.elif_(variable <= -MAX_OFFSET): - qua.set_dc_offset(name, "single", -MAX_OFFSET) + qua.set_dc_offset(element, "single", -MAX_OFFSET) with qua.else_(): - qua.set_dc_offset(name, "single", variable) + qua.set_dc_offset(element, "single", variable) -def _frequency(variable: _Variable, channel: Channel, configs: dict[str, Config]): - name = str(channel.name) - lo_frequency = configs[channel.lo].frequency - qua.update_frequency(name, variable - lo_frequency) +def _frequency(variable: _Variable, parameters: Parameters, element: ChannelId): + qua.update_frequency(element, variable - parameters.lo_frequency) INT_TYPE = {Parameter.frequency, Parameter.duration, Parameter.duration_interpolated}