Sweeps#

This notebooks explains sweeping in Pulla, that is, performing a loop over a settings value. It is recommended to run Configuration and Usage.ipynb before this notebook.

%matplotlib inline
import numpy as np
from copy import deepcopy
from pprint import pprint
from IPython.core.display import HTML

from qiskit import QuantumCircuit, visualization
from qiskit.compiler import transpile

from iqm.iqm_client.util import print_env_vars
from iqm.qiskit_iqm import IQMProvider
from iqm.pulla.pulla import Pulla
from iqm.pulla.utils_qiskit import qiskit_to_pulla, sweep_job_to_qiskit, get_qiskit_compiler
from iqm.pulse.playlist.visualisation.base import inspect_playlist
from exa.common.control.sweep.sweep import Sweep
from exa.common.control.sweep.option import CenterSpanOptions
from iqm.pulse.timebox import TimeBox
from iqm.pulse.builder import ScheduleBuilder
from iqm.pulse.playlist.schedule import Segment, Schedule
from iqm.pulse.playlist.instructions import Block
print_env_vars()

pulla = Pulla()
backend = IQMProvider().get_backend()

Qiskit Compiler#

When running Qiskit circuits through IQM Pulla, one way is to first transpile the circuit manually in your notebook/script, convert the circuit to the IQM representation, and finally compile the circuit for execution.

However, when working on the pulse level, one usually wants to control the routing and transpilation exactly (e.g. route to specific qubits in the chip), whereas the Qiskit transpiler is stochastic in nature. For facilitating pulse-level access in the context of Qiskit, one can use the function get_qiskit_compiler. It returns a Pulla compiler which contains the transpilation and conversion to IQM format compiler passes, so that users may just provide native Qiskit QuantumCircuits directly. It also allows easy routing to a subset of a chip.

compiler = get_qiskit_compiler(pulla, backend)

# Show the settings for the transpilation stage pass
compiler.get_settings(circuits=[]).stages.qiskit_transpilation.parallelize_and_transpile

(stages.qiskit_transpilation.parallelize_and_transpile)

  • stages.qiskit_transpilation.parallelize_and_transpile: (Name: "stages.qiskit_transpilation.parallelize_and_transpile", class:SettingNode) 0
    perform_move_routing True perform_move_routing
    optimize_single_qubits True optimize_single_qubits
    ignore_barriers_in_1qb_optimization False ignore_barriers_in_1qb_optimization
    remove_final_rzs True remove_final_rzs
    existing_moves_handling not set/auto existing_moves_handling
    optimization_level 0 optimization_level
    seed_transpiler not set/auto seed_transpiler
    num_processes not set/auto num_processes
  • # Display the docstring for the stage pass
    compiler.show_stages(pass_name="parallelize_and_transpile")
    
    Circuit-level Stages (3)
    Stage 0: qiskit_transpilation Transpile and route Qiskit circuits to the correct architecture.
    Pass 0: parallelize_and_transpile list[list[QuantumCircuit]]
    Full Signature
    (
    circuits: 'list[QuantumCircuit]',components: 'ComponentGrouping | None',context: 'dict[str, Any]',perform_move_routing: 'bool' = True,optimize_single_qubits: 'bool' = True,ignore_barriers_in_1qb_optimization: 'bool' = False,remove_final_rzs: 'bool' = True,existing_moves_handling: 'str | None' = None,optimization_level: 'int' = 0,seed_transpiler: 'int | None' = None,num_processes: 'int | None' = None) -> list[list[QuantumCircuit]]
    Documentation
    Transpile Qiskit circuits and parallelize them if colour grouped components were inputted. Args: circuits: List of Qiskit QuantumCircuit objects to transpile and potentially parallelize. components: List of (physical) components on which to transpile (route) the circuits. If a flat list of components is provided, the IQMTarget will be built only on that subset of the full QPU. If colour grouped components (i.e. of the form ``list[list[tuple(str, ...)]]``) is provided, the circuits will be parallelized such that each colour group becomes its own circuit, and the circuit will be broadcasted to parallel groups within a colour group, i.e. executed parallelly. If ``None`` is provided, the default target for the full QPU will be used. context: The Compiler context. perform_move_routing: Whether to perform MOVE gate routing. optimize_single_qubits: Whether to optimize single qubit gates away. ignore_barriers_in_1qb_optimization: Whether to ignore barriers when optimizing single qubit gates. remove_final_rzs: Whether to remove the final z rotations. existing_moves_handling: How to handle existing MOVE gates in the circuit, required if the circuit contains MOVE gates. optimization_level: The optimization level of the Qiskit transpiler. seed_transpiler: The seed of the Qiskit transpiler. num_processes: The number of parallel processes to use. Returns: Transpiled and possibly parallelized circuits. The circuit(s) in each inner list are executed in parallel. If there is no parallelization, each inner list has just one item.
    Stage 1: qiskit_to_iqm Convert Qiskit circuits into the internal circuit representation.
    Pass 0: qiskit_circuits_to_iqm_circuits list[Circuit]
    Full Signature
    (
    circuits: 'list[list[QuantumCircuit]]',components: 'ComponentGrouping | None',context: 'dict[str, Any]') -> list[Circuit]
    Documentation
    Convert Qiskit QuantumCircuits to IQM circuits. Args: circuits: Qiskit QuantumCircuit objects to compile. The circuits in each inner list are executed in parallel. components: Physical components on which to compile the circuits. If ``None``, will use the default IQMTarget in the Qiskit backend, otherwise restricts to these components. context: The Compiler context. Returns: Converted IQM circuits.
    Stage 2: circuit_stage Perform Circuit-level transformations.
    Pass 0: validate_settings list[iqm.pulse.circuit_operations.Circuit]
    Full Signature
    (
    circuits: list[iqm.pulse.circuit_operations.Circuit],settings: exa.common.data.setting_node.SettingNode) -> list[iqm.pulse.circuit_operations.Circuit]
    Documentation
    Validate the settings for circuit execution options (only some combinations make sense). Raises an error if full MOVE gate tracking is used without move gate validation. Raises a warning if terminal measurements would be used with active reset. Args: circuits: The circuits to compiler. settings: The settings tree to validate. Returns: The circuits as they were. Raises: CircuitError: if full MOVE gate tracking is used without move gate validation.
    Pass 1: map_components Iterable[iqm.pulse.circuit_operations.Circuit]
    Full Signature
    (
    circuits: collections.abc.Iterable[iqm.pulse.circuit_operations.Circuit],component_mapping: dict[str, str] | None,context: dict[str, typing.Any]) -> Iterable[iqm.pulse.circuit_operations.Circuit]
    Documentation
    Maps the logical component names in a sequence of instructions to the corresponding physical component names. Modifies ``instructions`` in place. If no mapping is provided, returns the circuits as they are. Args: circuits: The circuits to compiler. component_mapping: Mapping of logical component names to physical component names. ``None`` means the identity mapping. context: The Compiler context.
    Pass 2: subscribe_and_probe list[iqm.pulse.circuit_operations.Circuit]
    Full Signature
    (
    circuits: list[iqm.pulse.circuit_operations.Circuit],settings: exa.common.data.setting_node.SettingNode,context: dict[str, typing.Any],additionally_subscribed_components: list[str],additionally_probed_components: list[str],probe_all: bool = True,convert_terminal_measurements: bool = True) -> list[iqm.pulse.circuit_operations.Circuit]
    Documentation
    Add additional terminal measurements to the circuit and modify measurement instruction arguments. The additional measurements can be subscribed to (i.e. they'd return measurement data) or be just probe pulses for potentially improving the terminal measurement fidelity in case the measurement calibration is not 100% factorizable. In addition, the pass hashes all readout keys since there is a data processing limit for the readout key length. The keys should then be unmapped in the return data post-processing. The terminal measurements can also be converted to the ``measure_fidelity`` operation which is calibrated to maximize the fidelity while not necessarily being projective (QNDness is typically not important for the terminal measurement). Args: circuits: The circuits to compile. settings: The settings tree. context: The Compiler context. additionally_subscribed_components: Additional components to measure in the terminal measurement (besides the ones explicitly measured in the circuit itself). additionally_probed_components: Additional components to send the probe pulse to besides the ones explicitly measured in the circuit itself). The measurement data will not be collected from these components. probe_all: Whether to send to probe pulse to all components in the terminal measurement (overrides ``additionally_probed_components``). convert_terminal_measurements: Whether to convert the terminal measurement data to the ``measure_fidelity`` operation that is calibrated to maximize the fidelity while not necessarily being QND. This option will be turned to ``False`` automatically if active reset is used (active reset is not reliable in the presence of leakage). Returns: The circuits with the aforementioned modifications.
    Pass 3: validate_circuits list[iqm.pulse.circuit_operations.Circuit]
    Full Signature
    (
    circuits: list[iqm.pulse.circuit_operations.Circuit],builder: iqm.pulse.builder.ScheduleBuilder,context: dict[str, typing.Any],move_gate_validation: bool = True,validate_prx: bool = True,validate_calset: bool = False) -> list[iqm.pulse.circuit_operations.Circuit]
    Documentation
    Validate circuits and aggerate metrics data from them. Args: circuits: The circuits to compile. builder: The ScheduleBuilder. context: The compiler context. move_gate_validation: Whether to do the move gate validation. validate_prx: Whether to do the validation. validate_calset: Whether to validate the calibration set (if the calibration point used is not from a calibration set, this validation might not make sense).
    Pulse-level Stages (5)
    Stage 0: circuit_resolution Resolve Circuits into TimeBoxes.
    Pass 0: resolve_circuits list[iqm.pulse.timebox.TimeBox]
    Full Signature
    (
    circuits: list[iqm.pulse.circuit_operations.Circuit] | list[iqm.pulse.timebox.TimeBox],builder: iqm.pulse.builder.ScheduleBuilder,scheduling_strategy: str = 'ASAP') -> list[iqm.pulse.timebox.TimeBox]
    Documentation
    Resolve the circuits to timeboxes. Args: circuits: The circuit to resolve. builder: The schedule builder. scheduling_strategy: Scheduling strategy to be used in the resolved TimeBoxes (see :class:`.TimeBox`). Returns: List of TimeBoxes (one TimeBox per circuit).
    Stage 1: timebox_stage Perform TimeBox-level transformations.
    Pass 0: multiplex_readout list[iqm.pulse.timebox.TimeBox]
    Full Signature
    (
    timeboxes: list[iqm.pulse.timebox.TimeBox],timebox_input: bool) -> list[iqm.pulse.timebox.TimeBox]
    Documentation
    Merge any MultiplexedProbeTimeBoxes inside a TimeBox representing a circuit. This pass optimizes a situation where multiple "measure" gates on disjoint set of loci exist sequentially in the circuit. Without optimization, each gate would result in a separate trigger event, which results in worse performance. For example, with the measurement instructions [M(QB1), M(QB2), M(QB3)], we'd first measure QB1, then QB2, then QB3. This optimization merges the measurement timeboxes, so that we'll measure QB1, QB2, and QB3 at the same time (if the hardware channel configuration allows it), corresponding to M(QB1, QB2, QB3). Goes through the children of `circuit_box`, and places them in the same temporal order. Whenever a MultiplexedProbeTimeBox is encountered (i.e. from a measure gate), it is merged with the previous pending MultiplexedProbeTimeBox and left pending. If any other box type with colliding loci is encountered, first places the pending MultiplexedProbeTimeBox. This essentially delays all measurements until the last possible moment. This stage pass is skipped if the circuits to be compiled were given in the TimeBox-level, as the logic within does not work for deep recursive TimeBoxes. Args: timeboxes: Timeboxes representing circuits. timebox_input: Whether the circuits were inputted already in the TimeBox format. Returns: New TimeBoxes with the same content, except with some MultiplexedProbeTimeBoxes merged.
    Pass 1: prepend_heralding list[iqm.pulse.timebox.TimeBox]
    Full Signature
    (
    timeboxes: list[iqm.pulse.timebox.TimeBox],builder: iqm.pulse.builder.ScheduleBuilder,context: dict[str, typing.Any],add_heralding: bool = False) -> list[iqm.pulse.timebox.TimeBox]
    Documentation
    Add the heralding measurement TimeBox to all circuits (locus: active components that can be measured). Args: timeboxes: Timeboxes representing circuits. builder: The ScheduleBuilder. context: Compiler context. add_heralding: Whether to add the heralding measurement timebox to the schedules. Returns: The timeboxes with the prepended heralding measurement.
    Pass 2: prepend_reset list[iqm.pulse.timebox.TimeBox]
    Full Signature
    (
    timeboxes: list[iqm.pulse.timebox.TimeBox],builder: iqm.pulse.builder.ScheduleBuilder,context: dict[str, typing.Any],active_reset_cycles: int | None = None) -> list[iqm.pulse.timebox.TimeBox]
    Documentation
    Add a reset timebox to all circuits for all active components. Args: timeboxes: TimeBoxes representing circuits. builder: The ScheduleBuilder. context: The compiler context. active_reset_cycles: Number of active reset cycles applied. `None` means no active reset cycles, in which case reset is done by relaxation (waiting). Returns: The timeboxes with the prepended reset.
    Stage 2: timebox_resolution Resolve TimeBoxes into Schedules.
    Pass 0: resolve_timeboxes list[iqm.pulse.playlist.schedule.Schedule]
    Full Signature
    (
    timeboxes: list[iqm.pulse.timebox.TimeBox],builder: iqm.pulse.builder.ScheduleBuilder,neighborhood: int = 1) -> list[iqm.pulse.playlist.schedule.Schedule]
    Documentation
    Resolve the timeboxes to schedules. Args: timeboxes: TimeBoxes representing circuits. builder: The ScheduleBuilder. neighborhood: The neighborhood for the scheduling (see: :meth:`.ScheduleBuilder.resolve_timebox`). Returns: The time-resolved schedules (one for each circuit).
    Stage 3: dynamical_decoupling Apply dynamical decoupling sequences to idle qubits in the Schedules.
    Pass 0: apply_dd_strategy list[iqm.pulse.playlist.schedule.Schedule]
    Full Signature
    (
    schedules: list[iqm.pulse.playlist.schedule.Schedule],builder: iqm.pulse.builder.ScheduleBuilder,context: dict[str, typing.Any],dd_is_disabled: bool = True,use_standard_dd_strategy: bool = True,DDStrategy_merge_contiguous_waits: bool = True,DDStrategy_target_qubits: list[str] | None = None,DDStrategy_skip_leading_wait: bool = True,DDStrategy_skip_trailing_wait: bool = True,DDStrategy_gate_sequences_ratio: list[int] | None = None,DDStrategy_gate_sequences_gate_pattern_xy: list[str] | None = None,DDStrategy_gate_sequences_align: list[str] | None = None) -> list[iqm.pulse.playlist.schedule.Schedule]
    Documentation
    Insert dynamical decoupling sequences into the schedules, if dynamical decoupling is enabled. DDStrategy can also be read from the Compiler context, from under the key `"DDStrategy"`. In this case, the strategy provided will override the DDStrategy options given as args to this function. Args: schedules: Schedules representing the compiled circuits. builder: The ScheduleBuilder. context: Compiler context. dd_is_disabled: Set to ``False`` to enable dynamical decoupling. use_standard_dd_strategy: Whether to use the standard decoupling strategy (overrides the below arguments). DDStrategy_target_qubits: The qubits to which DD is applied (``None`` means apply to every applicable qubit). DDStrategy_merge_contiguous_waits: Whether to merge contiguous waits (see :class:`.DDStrategy`). DDStrategy_skip_leading_wait: Whether to skip leading waits (see :class:`.DDStrategy`). DDStrategy_skip_trailing_wait: Whether to skip trailing waits (see :class:`.DDStrategy`). DDStrategy_gate_sequences_ratio: Minimal durations for the Wait to be replaced with the DD sequence (in PRX gate durations) in the DD sequence. DDStrategy_gate_sequences_gate_pattern_xy: XY Gate patterns in the DD sequence. If you want to provide custom PRX angles instead of XY patterns, you must provide the DDStrategy in the Compiler context. DDStrategy_gate_sequences_align: Alignments in the DD sequence ("asap", "alap" or "center") Returns: THe schedules where applicable Waits are replaced with DD sequences.
    Stage 4: schedule_stage Perform Schedule-level transformations.
    Pass 0: apply_move_gate_phase_corrections list[iqm.pulse.playlist.schedule.Schedule]
    Full Signature
    (
    schedules: list[iqm.pulse.playlist.schedule.Schedule],builder: iqm.pulse.builder.ScheduleBuilder,context: dict[str, typing.Any],move_gate_frame_tracking_mode: str = 'full') -> list[iqm.pulse.playlist.schedule.Schedule]
    Documentation
    Apply calibrated phase corrections if MOVE gates are used.
    Pass 1: clean_schedule list[iqm.pulse.playlist.schedule.Schedule]
    Full Signature
    (
    schedules: list[iqm.pulse.playlist.schedule.Schedule],builder: iqm.pulse.builder.ScheduleBuilder) -> list[iqm.pulse.playlist.schedule.Schedule]
    Documentation
    Remove non-functional instructions from `schedules`.
    Finalization Stages (2)
    Stage 0: schedule_resolution Translate Schedules into a hardware-executable Playlist.
    Pass 0: build_playlist_and_merge_contexts iqm.models.playlist.playlist.Playlist
    Full Signature
    (
    schedules: list[iqm.pulse.playlist.schedule.Schedule],builder: iqm.pulse.builder.ScheduleBuilder,context: dict[str, typing.Any],compute_execution_time_metrics: bool = False,ignore_input_components: bool = False) -> iqm.models.playlist.playlist.Playlist
    Documentation
    Build the playlist from the schedules and merge the contexts for individual sweep spots. When merging the context, the active components are the union of the active components in each sweep spot (unless explicitly given by the user). The default context keys are not merged (these should not be modified by any of the sweep spots), and any other keys in the context will be merged to mapping from the sweep spot id to the context entry. Args: schedules: Schedules to build into a Playlist. builder: The ScheduleBuilder. context: The Compiler context. compute_execution_time_metrics: Whether to compute schedule duration and minimum execution time for circuits. ignore_input_components: If True, components are always replaced with the ones found in context. Returns: The Playlist containing schedules.
    Stage 1: job_creation Package the Playlist and metadata into a job description to be submitted to IQM Server.
    Pass 0: create_run_definition iqm.station_control.interface.models.run.RunDefinition
    Full Signature
    (
    playlist: iqm.models.playlist.playlist.Playlist,context: dict[str, typing.Any],data_size_safety_switch: bool = True,force_ragged_data: bool = False) -> iqm.station_control.interface.models.run.RunDefinition
    Documentation
    Create MQE-style RunDefinition. Args: playlist: The Playlist to create the RunDefinition for. context: The Compiler context. data_size_safety_switch: Whether to throw an error with exceedingly large return data sizes (set to ``False`` if you want to still run the job and are sure the DB and/or the stack can handle it). force_ragged_data: Whether to force the return data to the sparse ragged format even if the dimensions are representable as a cartesian product. Returns: The RunDefinition.
    Post-processing Stages (2)
    Stage 0: construct_run_result Aggregate raw hardware data into a structured run result object.
    Pass 0: construct_run_result iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    pulla_data: iqm.cpc.compiler.post_process.PullaData,context: dict[str, typing.Any]) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Construct the dataset and attach it to a RunResult object. Unmaps the hashed readout keys. Args: pulla_data: The Pulla data (run data and sweep data). context: The Compiler context. Returns: The RunResult.
    Stage 1: construct_data_variables Map measured counts and states to high-level user variables.
    Pass 0: create_ragged_data_index iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Adds ``MultiIndex`` to the trigger index dimension for accessing ragged data via the hard sweep values. If there is a non-uniform number of readout triggers for n RO label across the segments of the playlist, the data cannot be reshaped into an N-dimensional box with the dims ``(, , ...)``. The data is then "ragged" and the only meaningful way to index the data is along the readout trigger index dimension. In order to be able to use this ragged data meaningfully, the user often wants access it via the known dimensions of the experiment run, i.e. the sweeps. For this reason we attach the values the hard sweeps took in each segment where a trigger happened to the trigger index dimension. An example: the playlist was generated from hard sweeps (there are no soft sweeps here): ``[Sweep(parameter=param1, data=[0, 1]), Sweep(parameter=param2, data=[0.1, 0.2, 0.3])]``, which means there are 2 * 3 = 6 segments. Let's say for a given RO label ``"QB1__key"`` we have 1 RO trigger in the first segment, 1 trigger in the third, and 2 in the final one (the rest have no triggers for this label). Thus, the returned data has the length 4, i.e. the trigger index goes from 0 to 3. Taking into account the convention in ordering sweep dimensions, the values of the sweeps for these trigger indices are then: 0 => ``(param2 = 0.1, param1 = 0)`` (in the 1st segment, both params assume their first values) 1 => ``(param2 = 0.3, param1 = 0)`` (in the 3rd segment param2 has its 3rd value, while param1 is still in its 1st) 2 => ``(param2 = 0.3, param1 = 1)`` (in the last segment, both params have their final values) 3 => ``(param2 = 0.3, param1 = 1)`` (there were 2 triggers in the last segment, so this has the same values as 2.) The ``MultiIndex`` allows then the data via the hard sweep values: ``exp.last_run["_readout].sel({QB1::param1: 0})`` gives the slice where ``param1 == 0`` Args: run: The run result. Returns: The RunResult with the ragged data variables now indexed.
    Pass 1: extract_counter_group_data iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Extracts the data for all counter readout groups into their own data variables in the dataset. Args: run: the run result. Returns: The run result with new data variables for each counter readout group added.
    Pass 2: rename_data_variables iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult,context: dict[str, typing.Any]) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Rename the data variables in the dataset and optionally change their data types. Args: run: The RunResult object. context: Compiler context. Returns: The post-processed run results with the data variables renamed.
    Pass 3: classify_two_states iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Classify the complex data using the calibrated threshold. If the observations are not available, a warning is logged. Args: run: The run result. Returns: The run result with the discriminated shots added.
    Pass 4: average_single_shot_data iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Average the shots (or more generally the average bins) into a new data variable in the dataset. If the data is already averaged, it is returned unchanged. Args: run: The RunResult object. Returns: The RunResult with the single shot data variables averaged.
    Pass 5: contrast_data iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Add contrast for several components/readouts. Contrast is calculated from complex integrated data with PCA. If a data variable is not complex (i.e. it is already discriminated) it is retained as it is. Args: run: The RunResult object. Returns: The RunResult with the contrast data variable added for all complex variables.
    Pass 6: add_counter_averaged_readout iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Add single-qubit excited state probabilities from the multi qubit counter results to the dataset. Args: run: The run result. Returns: The run result with new data variables for averaged readout for each component added.
    Pass 7: compute_excitation_probability_from_data iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Compute the excited state probability for several components. For complex readout data, this is done using the averaged threshold readout observations and for threshold discriminated data using the 01 and 10 assignment error observations. If the aforementioned observations are not available, a warning is thrown. Args: run: The RunResult object. Returns: The RunResult with the excited state probability added into the dataset.
    Pass 8: add_counts_for iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Add single-qubit excited state probabilities from the multi qubit counter results to the dataset. This is done only if the dimensions of the single qubit probabilities are equal. Args: run: The run result. Returns: The run result with the counter data added.
    Pass 9: rename_measure_ancilla_variables iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Rename composite measure labels in data variables. A composite measure label is of the format `"__"` which is mapped into the usual format "__". Args: run: The run result. Returns: The run result with the renamed measure labels applied.
    Pass 10: process_metadata iqm.cpc.core.run_result.RunResult
    Full Signature
    (
    run: iqm.cpc.core.run_result.RunResult) -> iqm.cpc.core.run_result.RunResult
    Documentation
    Process the metadata in the RunResult. - Set the target data variables - Process tuples serialised into lists back into tuples. Args: run: The RunResult. Returns: The RunResult with the metadata processed.
    # Compile a Qiskit circuit while fixing some of the transpilation features
    qc = QuantumCircuit(2, 2)
    qc.x(0)
    qc.x(0)
    qc.h(0)
    qc.cx(0, 1)
    qc.measure_all()
    
    settings = compiler.get_settings(circuits=[qc])
    settings.stages.qiskit_transpilation.parallelize_and_transpile.seed_transpiler = 1  # fix the seed for more deterministic behaviour
    settings.stages.qiskit_transpilation.parallelize_and_transpile.optimize_single_qubits = False  # do not remove the two redundant x(0)s
    
    job_definition, context = compiler.compile(
        circuits=[qc],
        components=["QB3", "QB5"],  # this routes the circuit to qubits QB3 and QB5
        settings=settings
    )
    
    # Visualize
    HTML(inspect_playlist(job_definition.sweep_definition.playlist, [0]))
    
    /home/ville/iqm/repot/continuous-delivery/.dev/environment/venv/lib/python3.11/site-packages/IPython/core/display.py:431: UserWarning: Consider using IPython.display.IFrame instead
      warnings.warn("Consider using IPython.display.IFrame instead")
    

    It should be noted that the transpilation options exposed in settings.stages.qiskit_transpilation.parallelize_and_transpile are not exhaustive. For highly customized transpilation one should still transpile manually and avoid the Qiskit compiler.

    Sweeps#

    The specified circuit will be executed with the specified values, resulting in a data point for each different value of the swept quantity. Every setting in the settings tree is in principle sweepable (sweeping some of them makes little sense in practice, but is still doable).

    Example: Investigate spectator errors#

    For optimal QPU performance, each physical component should be “parked” to a flux voltage value that minimizes parasitic interactions with the neighbouring components. The parking is part of the standard calibration of an IQM QPU, but we can investigate its effects by running a two-qubit gate on a connected pair while sweeping a neighbouring coupler’s flux voltage. We should see the gate performance getting worse as we get further away from the correct parking voltage.

    Create four circuits to prepare the four states |00>, |01> |10> and |11> and then perform a CNOT gate between the two qubits. CNOT should map |10> to |11> and |11> to |10>, while leaving the other two states intact. We can measure the probability of being in the correct state in all of these cases, which gives us a rough estimate of how much the bad parking affected the gate operation (it should be noted that this is not a scientifically sound method of state/process tomography, but just a simple example for educational purposes).

    compiler = get_qiskit_compiler(pulla, backend)
    
    # Prepare |00>
    qc1 = QuantumCircuit(2, 2) 
    qc1.cx(0,1)
    qc1.measure([0,1], [0,1])
        
    # Prepare |10>
    qc2 = QuantumCircuit(2, 2) 
    qc2.x(0)
    qc2.cx(0,1)
    qc2.measure([0,1], [0,1])
        
    # Prepare |01>
    qc3 = QuantumCircuit(2, 2) 
    qc3.x(1)
    qc3.cx(0,1)
    qc3.measure([0,1], [0,1])
        
    # Prepare |11>
    qc4 = QuantumCircuit(2, 2)
    qc4.x(0)
    qc4.x(1)
    qc4.cx(0,1)
    qc4.measure([0,1], [0,1])
    
    circuits=[qc1, qc2, qc3, qc4]
    
    # First compile without sweeps
    settings = compiler.get_settings(circuits=circuits)
    job_definition, context = compiler.compile(
        circuits=circuits,
        components=["QB3", "QB5"],  # run on the pair 3-5
        settings=settings
    )
    
    # Run the circuits
    job = pulla.submit_playlist(job_definition, context=context)
    job.wait_for_completion()
    
    # Instead of the default `job.result()` run the post-processing stages in `compiler` to get the return data in an Xarray Dataset
    result = job.result(compiler)
    result.dataset
    
    [06-08 17:00:40;I] Waiting for job 019ea789-16f8-78d3-acfd-4da3602d4435 to finish...
    
    <xarray.Dataset> Size: 72kB
    Dimensions:                                 (circuit_index: 4,
                                                 repetitions: 1000, counter_index: 4)
    Coordinates:
      * circuit_index                           (circuit_index) int64 32B 0 1 2 3
      * repetitions                             (repetitions) int64 8kB 0 1 ... 999
      * counter_index                           (counter_index) int64 32B 0 1 2 3
    Data variables:
        QB3__c_2_0_0_state_single_shot          (circuit_index, repetitions) float64 32kB ...
        QB5__c_2_0_1_state_single_shot          (circuit_index, repetitions) float64 32kB ...
        QB3__c_2_0_0_readout                    (circuit_index) float64 32B 0.052...
        QB5__c_2_0_1_readout                    (circuit_index) float64 32B 0.124...
        QB3__c_2_0_0_excited_state_probability  (circuit_index) float64 32B 0.009...
        QB5__c_2_0_1_excited_state_probability  (circuit_index) float64 32B 0.070...
        counter.result                          (counter_index, circuit_index) float64 128B ...
    # In the result dataset, find the bit string probabilities from the data variable "counter.result", and pick the result
    # corresponding to each of the four created circuits. Compare the obtained result to the expected theoretical result.
    result_for_00 = result.dataset["counter.result"].isel({"circuit_index": 0}).data
    print(f"|00> should be mapped to [1.0, 0.0, 0.0, 0.0], we got {result_for_00}, the error is {1-result_for_00[0]}")
    result_for_10 = result.dataset["counter.result"].isel({"circuit_index": 1}).data
    print(f"|10> should be mapped to [0.0, 0.0, 0.0, 1.0], we got {result_for_10}, the error is {1-result_for_10[3]}")
    result_for_01 = result.dataset["counter.result"].isel({"circuit_index": 2}).data
    print(f"|01> should be mapped to [0.0, 1.0, 0.0, 0.0], we got {result_for_01}, the error is {1-result_for_01[1]}")
    result_for_11 = result.dataset["counter.result"].isel({"circuit_index": 3}).data
    print(f"|11> should be mapped to [0.0, 0.0, 1.0, 0.0], we got {result_for_11}, the error is {1-result_for_11[2]}")
    
    |00> should be mapped to [1.0, 0.0, 0.0, 0.0], we got [0.859 0.089 0.017 0.035], the error is 0.14100000000000001
    |10> should be mapped to [0.0, 0.0, 0.0, 1.0], we got [0.043 0.047 0.08  0.83 ], the error is 0.17000000000000004
    |01> should be mapped to [0.0, 1.0, 0.0, 0.0], we got [0.092 0.853 0.038 0.017], the error is 0.14700000000000002
    |11> should be mapped to [0.0, 0.0, 1.0, 0.0], we got [0.049 0.041 0.811 0.099], the error is 0.18899999999999995
    
    # Define the sweep over the couper flux voltage (centered at the correct value, span 0.5V, 30 points)
    settings = compiler.get_settings(circuits=circuits)
    voltage = settings.controllers["TC-2-3"].flux.voltage
    voltage_sweep = voltage.sweep(np.linspace(-0.5, 0.5, 30) + voltage.value)
    
    # Compile and execute
    job_definition, context = compiler.compile(
        circuits=circuits,
        components=["QB3", "QB5"],  # run on the pair 3-5
        settings=settings,
        sweeps=[voltage_sweep]
    )
    
    # Run the circuits
    job = pulla.submit_playlist(job_definition, context=context)
    job.wait_for_completion()
    result = job.result(compiler)
    
    [06-08 17:00:54;I] Waiting for job 019ea789-4cce-7df0-816b-fc8e0aeb76ec to finish...
    
    # Plot the resulting errors as a function of the flux voltage with native Xarray plotting (Matplotlib) 
    error_for_00 = 1.0 - result.dataset["counter.result"].isel({"circuit_index": 0, "counter_index": 0})
    error_for_10 = 1.0 - result.dataset["counter.result"].isel({"circuit_index": 1, "counter_index": 3})
    error_for_01 = 1.0 - result.dataset["counter.result"].isel({"circuit_index": 2, "counter_index": 1})
    error_for_11 = 1.0 - result.dataset["counter.result"].isel({"circuit_index": 3, "counter_index": 2})
    cumulative_error = error_for_00 + error_for_10 + error_for_01 + error_for_11
    cumulative_error.plot()
    
    [<matplotlib.lines.Line2D at 0x749583c084d0>]
    
    _images/71378d2ec75c342a2b413a489d24bebc0744a85706a834fe312c87d853944b31.png

    Example: Parametric circuits and multi-dimensional sweeps#

    You might have wondered why compiler.get_settings(circuits) requires the circuits as an argument. The reason is that the circuit can be a function that creates the actual circuits. The arguments of the circuit generation function then become settings and are thus sweepable, like any other settings.

    Compilation also works with multiple sweeps and sweep settings in parallel.

    def _my_circuit(n_cz: int = 0) -> list[QuantumCircuit]:
        """Perform ``n_cz`` CZ gates on a pair of qubits."""
        qc = QuantumCircuit(2, 2)
        qc.h(0)
        qc.h(1)
        for _ in range(n_cz):
            qc.cz(0, 1)
        qc.measure_all()
        return [qc]
    
    settings = compiler.get_settings(circuits=_my_circuit)
    
    # n_cz is now a setting in circuit
    settings.stages.circuit_generation.circuit
    

    (circuit)

  • circuit: (Name: "circuit", class:SettingNode) 0
    n_cz 0 n_cz
  • # Sweep over n_cz
    n_cz_sweep = Sweep(parameter=settings.stages.circuit_generation.circuit.n_cz.parameter, data=np.array([1,2,3]))
    
    # Sweep over the default PRX amplitude in parallel for the two qubits such that in a single sweep we sweep both amplitudes
    amp_sweep = tuple(
        Sweep(parameter=settings.get_gate_node_for_locus("prx", q).amplitude_i.parameter, data=np.array([0.1, 0.2])) for q in ["QB3", "QB5"]
    )
    
    # Compile with both sweeps. With multiple sweeps like this the "Cartesian product" of the sweep dimensions is executed,
    # such that:
    # 1st sweep spot: (n_cz=1, amps=0.1)
    # 2nd sweep spot: (n_cz=2, amps=0.1)
    # ...
    # 4th sweep spot: (n_cz=1, amps=0.2)
    # ...
    # 6th sweep spot: (n_cz=3, amps=0.2)
    
    job_definition, context = compiler.compile(
        circuits=_my_circuit,
        components=["QB3", "QB5"],
        sweeps=[amp_sweep, n_cz_sweep]
    )
    HTML(inspect_playlist(job_definition.sweep_definition.playlist, list(range(6))))
    
    /home/ville/iqm/repot/continuous-delivery/.dev/environment/venv/lib/python3.11/site-packages/IPython/core/display.py:431: UserWarning: Consider using IPython.display.IFrame instead
      warnings.warn("Consider using IPython.display.IFrame instead")
    

    Example: Advanced pulse-level access with TimeBox inputs#

    IQM Pulla enables pulse-level access for circuits written on the gate level (e.g. Qiskit circuits), but there are phenomena that simply cannot be studied with a gate-level circuit. For this reason, Pulla allows compiling circuits written in the native IQM TimeBox intermediate representation.

    As an example, compile and run a single-qubit circuit that consists of

    1. measuring a qubit,

    2. performing an X180 gate, and

    3. measuring again,

    but make the X180 pulse coincide with the first measurement. Then sweep the exact timing of the drive pulse relative to the beginning of the probe instruction. Of course, reading out a qubit while driving an X rotation probably breaks both of the operations and not achieve an X180 rotation. But sweeping the offset from the probe’s start, at some point the actual physical probe pulse has ended and the qubit is projected to the ground state and can perform the X180 rotation. If there is some buffer at the end of the probe instruction (after the physical probe pulse has ended), a more efficient timing is achievable by overlapping the X180 with the probe instruction.

    Performing this experiment in the circuit-level is simply impossible. The measurement operation will block its locus (QB1) for its full duration when compiling from the gate-level (as it should for consistent operation), so it cannot overlap with the drive pulse. But working natively in the pulse-level it can!

    def _drive_and_probe_overlap_circuit(builder: ScheduleBuilder, offset: int) -> list[TimeBox]:
        """
        Drive and probe pulse overlapping in a circuit.
        
        Args:
            builder: The ScheduleBuilder (Compiler's default tool for pulse-level compilation).
            offset: When the drive pulse starts relative from the start of the probe pulse. In integer samples (sampling rate 2 GHz).
        """
        m1 = TimeBox.composite(builder.measure(("QB1",))(key="m1"))
        m2 = TimeBox.composite(builder.measure(("QB1",))(key="m2"))
        prx = builder.prx(("QB1",)).rx(np.pi)
        # Put PRX "inside" the probe TimeBox so the instructions overlap
        drive_channel_name = builder.get_drive_channel("QB1")
        prx_instruction = prx.atom[drive_channel_name][0]
        probe_schedule = deepcopy(builder.resolve_timebox(m1, neighborhood=0))  # deepcopy, otherwise the cached result would get altered
        probe_schedule[drive_channel_name] = Segment([Block(offset), prx_instruction])
        probe_and_prx_box = TimeBox.atomic(probe_schedule, locus_components=("QB1",), label="probe and prx for QB1")
        return [probe_and_prx_box + m2]
    
    compiler = pulla.get_standard_compiler()
    settings = compiler.get_settings(timeboxes=_drive_and_probe_overlap_circuit)
    offset_sweep = Sweep(parameter=settings.stages.circuit_generation.circuit.offset.parameter, data=320 + 64*np.array(range(60)))
    
    job_definition, context = compiler.compile(
        timeboxes=_drive_and_probe_overlap_circuit,
        components=["QB1"],
        sweeps=[offset_sweep],
    )
    
    # Inspect the playlist and see that the drive pulse overlaps with the probe instruction and moves to the right with the sweep
    HTML(inspect_playlist(job_definition.sweep_definition.playlist, list(range(60))))
    
    job = pulla.submit_playlist(job_definition, context=context)
    job.wait_for_completion()
    result = job.result(compiler)
    
    # Plot the QB1 excited state probability in the second measurement
    result.dataset["QB1__m2_excited_state_probability"].plot()
    
    [06-08 17:01:59;I] Waiting for job 019ea78a-4e1e-7593-98c4-2a0f3b8f6e74 to finish...
    
    [<matplotlib.lines.Line2D at 0x749571205e50>]
    
    _images/cd1fc63b13a565f13815993d77dde9ecadeb489a6d42852f274156021fa9f29c.png