diff --git a/doc/source/getting-started/experiment.rst b/doc/source/getting-started/experiment.rst index 6d2751751..4991c49f4 100644 --- a/doc/source/getting-started/experiment.rst +++ b/doc/source/getting-started/experiment.rst @@ -208,7 +208,7 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her acquisition_type=AcquisitionType.INTEGRATION, ) - results = platform.execute([sequence], options, sweeper) + results = platform.execute([sequence], options, [[sweeper]]) # plot the results amplitudes = results[ro_pulse.id][0].magnitude diff --git a/doc/source/main-documentation/qibolab.rst b/doc/source/main-documentation/qibolab.rst index 50536e71e..4794c7e59 100644 --- a/doc/source/main-documentation/qibolab.rst +++ b/doc/source/main-documentation/qibolab.rst @@ -496,7 +496,7 @@ A tipical resonator spectroscopy experiment could be defined with: type=SweeperType.OFFSET, ) - results = platform.execute([sequence], options, sweeper) + results = platform.execute([sequence], options, [[sweeper]]) .. note:: @@ -543,7 +543,7 @@ For example: type=SweeperType.FACTOR, ) - results = platform.execute([sequence], options, sweeper_freq, sweeper_amp) + results = platform.execute([sequence], options, [[sweeper_freq], [sweeper_amp]]) Let's say that the RX pulse has, from the runcard, a frequency of 4.5 GHz and an amplitude of 0.3, the parameter space probed will be: diff --git a/doc/source/tutorials/calibration.rst b/doc/source/tutorials/calibration.rst index 1588ad654..375a52141 100644 --- a/doc/source/tutorials/calibration.rst +++ b/doc/source/tutorials/calibration.rst @@ -65,7 +65,7 @@ We then define the execution parameters and launch the experiment. acquisition_type=AcquisitionType.INTEGRATION, ) - results = platform.execute([sequence], options, sweeper) + results = platform.execute([sequence], options, [[sweeper]]) In few seconds, the experiment will be finished and we can proceed to plot it. @@ -153,7 +153,7 @@ We can now proceed to launch on hardware: acquisition_type=AcquisitionType.INTEGRATION, ) - results = platform.execute([sequence], options, sweeper) + results = platform.execute([sequence], options, [[sweeper]]) amplitudes = results[readout_pulse.id][0].magnitude frequencies = np.arange(-2e8, +2e8, 1e6) + drive_pulse.frequency diff --git a/src/qibolab/instruments/dummy.py b/src/qibolab/instruments/dummy.py index cc67b56d8..b9645ac21 100644 --- a/src/qibolab/instruments/dummy.py +++ b/src/qibolab/instruments/dummy.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Dict, List, Optional +from typing import Dict, Optional import numpy as np from qibo.config import log @@ -12,7 +12,7 @@ ) from qibolab.pulses import PulseSequence from qibolab.qubits import Qubit, QubitId -from qibolab.sweeper import Sweeper +from qibolab.sweeper import ParallelSweepers from qibolab.unrolling import Bounds from .abstract import Controller @@ -120,16 +120,18 @@ def play( couplers: Dict[QubitId, Coupler], sequence: PulseSequence, options: ExecutionParameters, - *sweepers: List[Sweeper], + sweepers: list[ParallelSweepers], ): results = {} if options.averaging_mode is not AveragingMode.CYCLIC: shape = (options.nshots,) + tuple( - len(sweeper.values) for sweeper in sweepers + min(len(sweep.values) for sweep in parsweeps) for parsweeps in sweepers ) else: - shape = tuple(len(sweeper.values) for sweeper in sweepers) + shape = tuple( + min(len(sweep.values) for sweep in parsweeps) for parsweeps in sweepers + ) for ro_pulse in sequence.ro_pulses: values = self.get_values(options, ro_pulse, shape) diff --git a/src/qibolab/platform/platform.py b/src/qibolab/platform/platform.py index 3dd122847..3fbdfe147 100644 --- a/src/qibolab/platform/platform.py +++ b/src/qibolab/platform/platform.py @@ -2,7 +2,8 @@ from collections import defaultdict from dataclasses import dataclass, field, fields -from typing import Any, Dict, List, Optional, Tuple +from math import prod +from typing import Any, Dict, List, Optional, Tuple, TypeVar import networkx as nx from qibo.config import log, raise_error @@ -13,7 +14,7 @@ from qibolab.pulses import Delay, Drag, PulseSequence, PulseType from qibolab.qubits import Qubit, QubitId, QubitPair, QubitPairId from qibolab.serialize_ import replace -from qibolab.sweeper import Sweeper +from qibolab.sweeper import ParallelSweepers from qibolab.unrolling import batch InstrumentMap = Dict[InstrumentId, Instrument] @@ -23,6 +24,15 @@ NS_TO_SEC = 1e-9 +# TODO: replace with https://docs.python.org/3/reference/compound_stmts.html#type-params +T = TypeVar("T") + + +# TODO: lift for general usage in Qibolab +def default(value: Optional[T], default: T) -> T: + """None replacement shortcut.""" + return value if value is not None else default + def unroll_sequences( sequences: List[PulseSequence], relaxation_time: int @@ -60,6 +70,23 @@ def unroll_sequences( return total_sequence, readout_map +def estimate_duration( + sequences: list[PulseSequence], + options: ExecutionParameters, + sweepers: list[ParallelSweepers], +) -> float: + """Estimate experiment duration.""" + duration = sum(seq.duration for seq in sequences) + relaxation = default(options.relaxation_time, 0) + nshots = default(options.nshots, 0) + return ( + (duration + len(sequences) * relaxation) + * nshots + * NS_TO_SEC + * prod(len(s[0].values) for s in sweepers) + ) + + @dataclass class Settings: """Default execution settings read from the runcard.""" @@ -231,14 +258,14 @@ def _controller(self): assert len(controllers) == 1 return controllers[0] - def _execute(self, sequence, options, *sweepers): + def _execute(self, sequence, options, sweepers): """Executes sequence on the controllers.""" result = {} for instrument in self.instruments.values(): if isinstance(instrument, Controller): new_result = instrument.play( - self.qubits, self.couplers, sequence, options, *sweepers + self.qubits, self.couplers, sequence, options, sweepers ) if isinstance(new_result, dict): result.update(new_result) @@ -249,11 +276,14 @@ def execute( self, sequences: List[PulseSequence], options: ExecutionParameters, - *sweepers: Sweeper, + sweepers: Optional[list[ParallelSweepers]] = None, ) -> dict[Any, list]: """Execute a pulse sequences. - If any sweeper is passed, the execution is performed for the different values of sweeped parameters. + If any sweeper is passed, the execution is performed for the different values + of sweeped parameters. + + Returns readout results acquired by after execution. Example: .. testcode:: @@ -271,26 +301,15 @@ def execute( pulse = platform.create_qubit_readout_pulse(qubit=0) sequence.append(pulse) parameter_range = np.random.randint(10, size=10) - sweeper = Sweeper(parameter, parameter_range, [pulse]) - platform.execute([sequence], ExecutionParameters(), sweeper) - - Args: - sequence (List[:class:`qibolab.pulses.PulseSequence`]): Pulse sequences to execute. - options (:class:`qibolab.platforms.platform.ExecutionParameters`): Object holding the execution options. - **kwargs: May need them for something - Returns: - Readout results acquired by after execution. + sweeper = [Sweeper(parameter, parameter_range, [pulse])] + platform.execute([sequence], ExecutionParameters(), [sweeper]) """ + if sweepers is None: + sweepers = [] + options = self.settings.fill(options) - duration = sum(seq.duration for seq in sequences) - time = ( - (duration + len(sequences) * options.relaxation_time) - * options.nshots - * NS_TO_SEC - ) - for sweep in sweepers: - time *= len(sweep.values) + time = estimate_duration(sequences, options, sweepers) log.info(f"Minimal execution time: {time}") # find readout pulses @@ -303,7 +322,7 @@ def execute( results = defaultdict(list) for b in batch(sequences, self._controller.bounds): sequence, readouts = unroll_sequences(b, options.relaxation_time) - result = self._execute(sequence, options, *sweepers) + result = self._execute(sequence, options, sweepers) for serial, new_serials in readouts.items(): results[serial].extend(result[ser] for ser in new_serials) diff --git a/src/qibolab/sweeper.py b/src/qibolab/sweeper.py index ddb17297a..4a49bc63f 100644 --- a/src/qibolab/sweeper.py +++ b/src/qibolab/sweeper.py @@ -45,9 +45,9 @@ class SweeperType(Enum): class Sweeper: """Data structure for Sweeper object. - This object is passed as an argument to the method :func:`qibolab.platforms.abstract.Platform.sweep` + This object is passed as an argument to the method :func:`qibolab.platforms.platform.Platform.execute` which enables the user to sweep a specific parameter for one or more pulses. For information on how to - perform sweeps see :func:`qibolab.platforms.abstract.Platform.sweep`. + perform sweeps see :func:`qibolab.platforms.platform.Platform.execute`. Example: .. testcode:: @@ -66,7 +66,7 @@ class Sweeper: sequence.append(pulse) parameter_range = np.random.randint(10, size=10) sweeper = Sweeper(parameter, parameter_range, [pulse]) - platform.sweep(sequence, ExecutionParameters(), sweeper) + platform.execute([sequence], ExecutionParameters(), [[sweeper]]) Args: parameter (`qibolab.sweeper.Parameter`): parameter to be swept, possible choices are frequency, attenuation, amplitude, current and gain. @@ -111,3 +111,7 @@ def __post_init__(self): def get_values(self, base_value): """Convert sweeper values depending on the sweeper type.""" return self.type.value(self.values, base_value) + + +ParallelSweepers = list[Sweeper] +"""Sweepers that should be iterated in parallel.""" diff --git a/tests/test_dummy.py b/tests/test_dummy.py index bff9e914e..de37612a3 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -117,7 +117,7 @@ def test_dummy_single_sweep_raw(name): averaging_mode=AveragingMode.CYCLIC, acquisition_type=AcquisitionType.RAW, ) - results = platform.execute([sequence], options, sweeper) + results = platform.execute([sequence], options, [[sweeper]]) assert pulse.id and pulse.qubit in results shape = results[pulse.qubit][0].magnitude.shape assert shape == (pulse.duration * SWEPT_POINTS,) @@ -162,7 +162,7 @@ def test_dummy_single_sweep_coupler( fast_reset=fast_reset, ) average = not options.averaging_mode is AveragingMode.SINGLESHOT - results = platform.execute([sequence], options, sweeper) + results = platform.execute([sequence], options, [[sweeper]]) assert ro_pulse.id and ro_pulse.qubit in results if average: @@ -208,7 +208,7 @@ def test_dummy_single_sweep(name, fast_reset, parameter, average, acquisition, n fast_reset=fast_reset, ) average = not options.averaging_mode is AveragingMode.SINGLESHOT - results = platform.execute([sequence], options, sweeper) + results = platform.execute([sequence], options, [[sweeper]]) assert pulse.id and pulse.qubit in results if average: @@ -270,7 +270,7 @@ def test_dummy_double_sweep(name, parameter1, parameter2, average, acquisition, acquisition_type=acquisition, ) average = not options.averaging_mode is AveragingMode.SINGLESHOT - results = platform.execute([sequence], options, sweeper1, sweeper2) + results = platform.execute([sequence], options, [[sweeper1], [sweeper2]]) assert ro_pulse.id and ro_pulse.qubit in results @@ -333,7 +333,7 @@ def test_dummy_single_sweep_multiplex(name, parameter, average, acquisition, nsh acquisition_type=acquisition, ) average = not options.averaging_mode is AveragingMode.SINGLESHOT - results = platform.execute([sequence], options, sweeper1) + results = platform.execute([sequence], options, [[sweeper1]]) for ro_pulse in ro_pulses.values(): assert ro_pulse.id and ro_pulse.qubit in results diff --git a/tests/test_result_shapes.py b/tests/test_result_shapes.py index 487da3091..3931d0373 100644 --- a/tests/test_result_shapes.py +++ b/tests/test_result_shapes.py @@ -35,7 +35,7 @@ def execute(platform: Platform, acquisition_type, averaging_mode, sweep=False): sweeper1 = Sweeper(Parameter.bias, amp_values, qubits=[platform.qubits[qubit]]) # sweeper1 = Sweeper(Parameter.amplitude, amp_values, pulses=[qd_pulse]) sweeper2 = Sweeper(Parameter.frequency, freq_values, pulses=[ro_pulse]) - results = platform.execute([sequence], options, sweeper1, sweeper2) + results = platform.execute([sequence], options, [[sweeper1], [sweeper2]]) else: results = platform.execute([sequence], options) return results[qubit][0]