From ed25d4c4b12881dd41d7ed2d0cdce8b2da6ad4ae Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Jul 2021 23:04:09 +0200 Subject: [PATCH 1/9] * First draft of transpiler functionality. --- .../calibration/management/transpiler.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 qiskit_experiments/calibration/management/transpiler.py diff --git a/qiskit_experiments/calibration/management/transpiler.py b/qiskit_experiments/calibration/management/transpiler.py new file mode 100644 index 0000000000..10c526db52 --- /dev/null +++ b/qiskit_experiments/calibration/management/transpiler.py @@ -0,0 +1,129 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Transpiler functionality for calibrations.""" + +from typing import Dict, List, Optional, Tuple, Union + +from qiskit.transpiler.passes import SetLayout +from qiskit.transpiler.passes import ( + TrivialLayout, + FullAncillaAllocation, + EnlargeWithAncilla, + ApplyLayout +) +from qiskit.transpiler.layout import Layout +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.coupling import CouplingMap +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.dagcircuit import DAGCircuit +from qiskit.pulse.schedule import ScheduleBlock +from qiskit.transpiler.basepasses import TransformationPass + +from qiskit_experiments.calibration.management.calibrations import Calibrations + +class CalibrationAdder(TransformationPass): + """Transformation pass to inject calibrations into circuits.""" + + def __init__( + self, + calibrations: Calibrations, + gate_schedule_map: Optional[Dict[str, str]] = None, + ): + """ + TODO Discuss: we could give calibrations as Dict[str, Dict[Tuple, ScheduleBlock]] + TODO but this means that we need to export all the calibrations which may not scale well + TODO using cas.get_schedule(name, qubits) on an as needed basis seems better. + + Args: + calibrations: + gate_schedule_map: + """ + super().__init__() + self._cals = calibrations + self._gate_schedule_map = gate_schedule_map or dict() + + def get_calibration( + self, + gate_name: str, + qubits: Tuple[int, ...] + ) -> Union[ScheduleBlock, None]: + """Gets the calibrated schedule + + Args: + gate_name: Name of the gate for which to get the schedule. + params: The parameters of the gate if any. + qubits: The qubits for which to get the parameters. + + Returns: + The schedule if one is found otherwise return None. + """ + name = self._gate_schedule_map.get(gate_name, gate_name) + try: + return self._cals.get_schedule(name, qubits) + except KeyError: + return None + + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the calibration adder pass on `dag`. + + Args: + dag: DAG to schedule. + + Returns: + A DAG with calibrations added to it. + """ + bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + + for node in dag.nodes(): + if node.type == "op": + params = node.op.params + qubits = tuple(bit_indices[qarg] for qarg in node.qargs) + + schedule = self.get_calibration(node.op.name, qubits) + + if schedule is not None: + dag.add_calibration(node.op, qubits, schedule, params=params) + + return dag + + +def get_calibration_pass_manager( + initial_layout: List[int], + coupling_map: List[List[int]], + calibrations: Optional[Calibrations] +) -> PassManager: + """Get a calibrations experiment pass manager. + + Args: + initial_layout: + coupling_map: + calibrations: + + Returns: + An instance of :class:`PassManager` tailored to calibration experiments. + """ + initial_layout = Layout.from_intlist(initial_layout, QuantumRegister(len(initial_layout), "q")) + coupling_map = CouplingMap(coupling_map) + + def _choose_layout_condition(property_set): + return not property_set["layout"] + + pm = PassManager() + pm.append(SetLayout(initial_layout)) + pm.append(TrivialLayout(coupling_map), condition=_choose_layout_condition) + pm.append([FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()]) + + if calibrations is not None: + pm.append(CalibrationAdder(calibrations)) + + return pm From 62744f23052bc025d285ea116d8d44182c03659c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Jul 2021 23:06:27 +0200 Subject: [PATCH 2/9] * Pass in the gate schedule map. --- .../calibration/management/transpiler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/calibration/management/transpiler.py b/qiskit_experiments/calibration/management/transpiler.py index 10c526db52..f9e52e4249 100644 --- a/qiskit_experiments/calibration/management/transpiler.py +++ b/qiskit_experiments/calibration/management/transpiler.py @@ -31,6 +31,8 @@ from qiskit_experiments.calibration.management.calibrations import Calibrations +# TODO: need to show how this might work in an experiment. E.g. Rabi? + class CalibrationAdder(TransformationPass): """Transformation pass to inject calibrations into circuits.""" @@ -98,9 +100,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: def get_calibration_pass_manager( - initial_layout: List[int], - coupling_map: List[List[int]], - calibrations: Optional[Calibrations] + initial_layout: List[int], + coupling_map: List[List[int]], + calibrations: Optional[Calibrations], + gate_schedule_map: Optional[Dict[str, str]], ) -> PassManager: """Get a calibrations experiment pass manager. @@ -108,6 +111,7 @@ def get_calibration_pass_manager( initial_layout: coupling_map: calibrations: + gate_schedule_map: Returns: An instance of :class:`PassManager` tailored to calibration experiments. @@ -124,6 +128,6 @@ def _choose_layout_condition(property_set): pm.append([FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()]) if calibrations is not None: - pm.append(CalibrationAdder(calibrations)) + pm.append(CalibrationAdder(calibrations, gate_schedule_map)) return pm From 7f23461105075a0c02f90dc86ab579a9f5cbf5f6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 15 Jul 2021 20:54:51 +0200 Subject: [PATCH 3/9] * Added example of how to inject calibrations in the EFRabi. --- .../calibration/management/transpiler.py | 31 +++++++++++++++---- qiskit_experiments/calibration/rabi.py | 12 +++++++ test/calibration/experiments/test_rabi.py | 24 ++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/calibration/management/transpiler.py b/qiskit_experiments/calibration/management/transpiler.py index f9e52e4249..a9e424f47a 100644 --- a/qiskit_experiments/calibration/management/transpiler.py +++ b/qiskit_experiments/calibration/management/transpiler.py @@ -30,8 +30,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit_experiments.calibration.management.calibrations import Calibrations +from qiskit_experiments.exceptions import CalibrationError -# TODO: need to show how this might work in an experiment. E.g. Rabi? class CalibrationAdder(TransformationPass): """Transformation pass to inject calibrations into circuits.""" @@ -40,24 +40,35 @@ def __init__( self, calibrations: Calibrations, gate_schedule_map: Optional[Dict[str, str]] = None, + qubit_layout: Optional[Dict[int, int]] = None ): """ + + This transpiler pass is intended to be run in the :meth:`circuits` method of the + experiment classes before the main transpiler pass. + TODO Discuss: we could give calibrations as Dict[str, Dict[Tuple, ScheduleBlock]] TODO but this means that we need to export all the calibrations which may not scale well - TODO using cas.get_schedule(name, qubits) on an as needed basis seems better. + TODO using cals.get_schedule(name, qubits) on an as needed basis seems better. Args: - calibrations: - gate_schedule_map: + calibrations: An instance of calibration from which to fetch the schedules. + gate_schedule_map: A dictionary to map gate names to schedule names in the + calibrations. If this is not provided the transpiler pass will assume that + the schedule has the same name as the gate. + qubit_layout: The initial layout that will be used. This remaps the qubits + in the added calibrations. For instance, if {0: 3} is given and use this pass + on a circuit then any gates on qubit 0 will add calibrations for qubit 3. """ super().__init__() self._cals = calibrations self._gate_schedule_map = gate_schedule_map or dict() + self._qubit_layout = qubit_layout def get_calibration( self, gate_name: str, - qubits: Tuple[int, ...] + qubits: Tuple[int, ...], ) -> Union[ScheduleBlock, None]: """Gets the calibrated schedule @@ -72,7 +83,7 @@ def get_calibration( name = self._gate_schedule_map.get(gate_name, gate_name) try: return self._cals.get_schedule(name, qubits) - except KeyError: + except CalibrationError: return None def run(self, dag: DAGCircuit) -> DAGCircuit: @@ -91,6 +102,12 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: params = node.op.params qubits = tuple(bit_indices[qarg] for qarg in node.qargs) + if self._qubit_layout is not None: + try: + qubits = tuple(self._qubit_layout[qubit] for qubit in qubits) + except KeyError: + pass + schedule = self.get_calibration(node.op.name, qubits) if schedule is not None: @@ -107,6 +124,8 @@ def get_calibration_pass_manager( ) -> PassManager: """Get a calibrations experiment pass manager. + TODO not sure if we need this. Maybe not. + Args: initial_layout: coupling_map: diff --git a/qiskit_experiments/calibration/rabi.py b/qiskit_experiments/calibration/rabi.py index e41ccf6645..c91f34ae9d 100644 --- a/qiskit_experiments/calibration/rabi.py +++ b/qiskit_experiments/calibration/rabi.py @@ -21,10 +21,12 @@ from qiskit.providers import Backend import qiskit.pulse as pulse from qiskit.providers.options import Options +from qiskit.transpiler.passmanager import PassManager from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.calibration.analysis.oscillation_analysis import OscillationAnalysis from qiskit_experiments.exceptions import CalibrationError +from qiskit_experiments.calibration.management.transpiler import CalibrationAdder class Rabi(BaseExperiment): @@ -220,6 +222,8 @@ def _default_experiment_options(cls) -> Options: schedule=None, normalization=True, frequency_shift=None, + x_schedule_name="x", + calibrations=None, ) def _default_gate_schedule(self, backend: Optional[Backend] = None): @@ -268,4 +272,12 @@ def _template_circuit(self, amp_param) -> QuantumCircuit: circuit.append(Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[amp_param]), (0,)) circuit.measure_active() + if self.experiment_options.calibrations is not None: + cals = self.experiment_options.calibrations + gate_schedule_map = {"x": self.experiment_options.x_schedule_name} + qubit_layout = {0: self.physical_qubits[0]} + cal_adder = PassManager(CalibrationAdder(cals, gate_schedule_map, qubit_layout)) + + circuit = cal_adder.run(circuit) + return circuit diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index ad002d5f9b..19e5359e16 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -27,6 +27,7 @@ from qiskit_experiments.calibration import Rabi, EFRabi from qiskit_experiments.calibration.analysis.oscillation_analysis import OscillationAnalysis +from qiskit_experiments.calibration.management.calibrations import Calibrations from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.nodes import Probability from qiskit_experiments.composite.parallel_experiment import ParallelExperiment @@ -179,6 +180,29 @@ def test_user_schedule(self): assigned_sched = my_schedule.assign_parameters({amp: 0.5}, inplace=False) self.assertEqual(circs[0].calibrations["Rabi"][((2,), (0.5,))], assigned_sched) + def test_transpile_x_gate(self): + """Test that we can use the calibrations to transpile in the x gate.""" + cals = Calibrations() + qubit = 3 + + chan = Parameter("ch0") + amp = Parameter("amp") + + with pulse.build(name="xp") as xp_sched: + pulse.play(pulse.Gaussian(123, amp, 25), pulse.DriveChannel(chan)) + + cals.add_schedule(xp_sched) + cals.add_parameter_value(0.2, "amp", schedule="xp") + + rabi = EFRabi(qubit) + rabi.set_experiment_options(calibrations=cals, x_schedule_name="xp", frequency_shift=-330e6) + + circs = rabi.circuits(RabiBackend()) + + expected = xp_sched.assign_parameters({amp: 0.2, chan: 3}, inplace=False) + + self.assertEqual(circs[0].calibrations["x"][(3, ), ()], expected) + class TestRabiAnalysis(QiskitTestCase): """Class to test the fitting.""" From 3d6b7a79c12d8868c8cf5ddb40f828569b56b5c6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 22 Jul 2021 14:26:20 +0200 Subject: [PATCH 4/9] * Moved file. --- .../management => calibration_management}/transpiler.py | 0 qiskit_experiments/library/calibration/rabi.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename qiskit_experiments/{calibration/management => calibration_management}/transpiler.py (100%) diff --git a/qiskit_experiments/calibration/management/transpiler.py b/qiskit_experiments/calibration_management/transpiler.py similarity index 100% rename from qiskit_experiments/calibration/management/transpiler.py rename to qiskit_experiments/calibration_management/transpiler.py diff --git a/qiskit_experiments/library/calibration/rabi.py b/qiskit_experiments/library/calibration/rabi.py index 554c504516..08fe1fc917 100644 --- a/qiskit_experiments/library/calibration/rabi.py +++ b/qiskit_experiments/library/calibration/rabi.py @@ -26,7 +26,7 @@ from qiskit_experiments.framework import BaseExperiment from qiskit_experiments.library.calibration.analysis.oscillation_analysis import OscillationAnalysis from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration.management.transpiler import CalibrationAdder +from qiskit_experiments.calibration_management.transpiler import CalibrationAdder class Rabi(BaseExperiment): From 8e2cf3ebcd00bedfe08be18c0e24d9fabe966269 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 22 Jul 2021 16:37:34 +0200 Subject: [PATCH 5/9] * Tailored the purpose of the changes to calibration experiments. --- .../calibration_key_types.py | 1 + .../calibration_management/transpiler.py | 87 ++++++++----------- .../library/calibration/rabi.py | 87 +++++++++---------- 3 files changed, 80 insertions(+), 95 deletions(-) diff --git a/qiskit_experiments/calibration_management/calibration_key_types.py b/qiskit_experiments/calibration_management/calibration_key_types.py index a1bdcccbc5..91d88a7dc4 100644 --- a/qiskit_experiments/calibration_management/calibration_key_types.py +++ b/qiskit_experiments/calibration_management/calibration_key_types.py @@ -20,4 +20,5 @@ ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) +InstructionMap = namedtuple("InstructionMap", ["inst", "schedule", "free_params"]) ParameterValueType = Union[ParameterExpression, float, int, complex] diff --git a/qiskit_experiments/calibration_management/transpiler.py b/qiskit_experiments/calibration_management/transpiler.py index a9e424f47a..ce9117c1b7 100644 --- a/qiskit_experiments/calibration_management/transpiler.py +++ b/qiskit_experiments/calibration_management/transpiler.py @@ -14,32 +14,25 @@ from typing import Dict, List, Optional, Tuple, Union -from qiskit.transpiler.passes import SetLayout -from qiskit.transpiler.passes import ( - TrivialLayout, - FullAncillaAllocation, - EnlargeWithAncilla, - ApplyLayout -) -from qiskit.transpiler.layout import Layout -from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.coupling import CouplingMap -from qiskit.circuit.quantumregister import QuantumRegister +from qiskit import QuantumCircuit from qiskit.dagcircuit import DAGCircuit from qiskit.pulse.schedule import ScheduleBlock from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passmanager import PassManager from qiskit_experiments.calibration.management.calibrations import Calibrations from qiskit_experiments.exceptions import CalibrationError +from qiskit_experiments.calibration_management.calibration_key_types import InstructionMap +from qiskit_experiments.framework.base_experiment import BaseExperiment -class CalibrationAdder(TransformationPass): +class CalAdder(TransformationPass): """Transformation pass to inject calibrations into circuits.""" def __init__( self, calibrations: Calibrations, - gate_schedule_map: Optional[Dict[str, str]] = None, + instruction_maps: Optional[List[InstructionMap]] = None, qubit_layout: Optional[Dict[int, int]] = None ): """ @@ -47,22 +40,23 @@ def __init__( This transpiler pass is intended to be run in the :meth:`circuits` method of the experiment classes before the main transpiler pass. - TODO Discuss: we could give calibrations as Dict[str, Dict[Tuple, ScheduleBlock]] - TODO but this means that we need to export all the calibrations which may not scale well - TODO using cals.get_schedule(name, qubits) on an as needed basis seems better. - Args: calibrations: An instance of calibration from which to fetch the schedules. - gate_schedule_map: A dictionary to map gate names to schedule names in the - calibrations. If this is not provided the transpiler pass will assume that - the schedule has the same name as the gate. + instruction_maps: A list of instruction maps to map gate names in the circuit to + schedule names in the calibrations. If this is not provided the transpiler pass + will assume that the schedule has the same name as the gate. Each instruction map + may also specify parameters that should be left free in the schedule. qubit_layout: The initial layout that will be used. This remaps the qubits in the added calibrations. For instance, if {0: 3} is given and use this pass on a circuit then any gates on qubit 0 will add calibrations for qubit 3. """ super().__init__() self._cals = calibrations - self._gate_schedule_map = gate_schedule_map or dict() + + self._instruction_maps = dict() + for inst_map in instruction_maps: + self._intruction_maps[inst_map.inst] = inst_map + self._qubit_layout = qubit_layout def get_calibration( @@ -74,15 +68,22 @@ def get_calibration( Args: gate_name: Name of the gate for which to get the schedule. - params: The parameters of the gate if any. qubits: The qubits for which to get the parameters. Returns: The schedule if one is found otherwise return None. """ - name = self._gate_schedule_map.get(gate_name, gate_name) + name = gate_name + assign_params = None + + # check for a non-trivial instruction to schedule mapping. + if gate_name in self._instruction_maps: + inst_map = self._instruction_maps[gate_name] + name = inst_map.schedule + assign_params = {param.name: param for param in inst_map.free_params} + try: - return self._cals.get_schedule(name, qubits) + return self._cals.get_schedule(name, qubits, assign_params=assign_params) except CalibrationError: return None @@ -116,37 +117,21 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: return dag -def get_calibration_pass_manager( - initial_layout: List[int], - coupling_map: List[List[int]], - calibrations: Optional[Calibrations], - gate_schedule_map: Optional[Dict[str, str]], -) -> PassManager: - """Get a calibrations experiment pass manager. +def inject_calibrations(circuit: QuantumCircuit, experiment: BaseExperiment) -> QuantumCircuit: + """Inject calibrations from a :class:`Calibrations` instance into a circuit. - TODO not sure if we need this. Maybe not. + This function requires that the experiment has a list of InstructionMaps in its + experiment options. Args: - initial_layout: - coupling_map: - calibrations: - gate_schedule_map: + circuit: The circuit into which to inject calibrations. + experiment: The experiment object that Returns: - An instance of :class:`PassManager` tailored to calibration experiments. + A quantum circuit with the relevant calibrations inject into it. """ - initial_layout = Layout.from_intlist(initial_layout, QuantumRegister(len(initial_layout), "q")) - coupling_map = CouplingMap(coupling_map) - - def _choose_layout_condition(property_set): - return not property_set["layout"] - - pm = PassManager() - pm.append(SetLayout(initial_layout)) - pm.append(TrivialLayout(coupling_map), condition=_choose_layout_condition) - pm.append([FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()]) - - if calibrations is not None: - pm.append(CalibrationAdder(calibrations, gate_schedule_map)) + calibrations = experiment.experiment_options.calibrations + inst_maps = experiment.experiment_options.instruction_name_maps + layout = {idx: qubit for idx, qubit in enumerate(experiment.physical_qubits)} - return pm + return PassManager(CalAdder(calibrations, inst_maps, layout)).run(circuit) diff --git a/qiskit_experiments/library/calibration/rabi.py b/qiskit_experiments/library/calibration/rabi.py index 08fe1fc917..9ac0b24ced 100644 --- a/qiskit_experiments/library/calibration/rabi.py +++ b/qiskit_experiments/library/calibration/rabi.py @@ -12,7 +12,7 @@ """Rabi amplitude experiment.""" -from typing import List, Optional +from typing import List, Optional, Tuple import numpy as np from qiskit import QuantumCircuit @@ -21,12 +21,12 @@ from qiskit.providers import Backend import qiskit.pulse as pulse from qiskit.providers.options import Options -from qiskit.transpiler.passmanager import PassManager from qiskit_experiments.framework import BaseExperiment from qiskit_experiments.library.calibration.analysis.oscillation_analysis import OscillationAnalysis from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management.transpiler import CalibrationAdder +from qiskit_experiments.calibration_management.transpiler import inject_calibrations +from qiskit_experiments.calibration_management.calibration_key_types import InstructionMap class Rabi(BaseExperiment): @@ -70,10 +70,10 @@ def _default_experiment_options(cls) -> Options: """ return Options( - duration=160, - sigma=40, amplitudes=np.linspace(-0.95, 0.95, 51), schedule=None, + instruction_name_maps=[InstructionMap(cls.__rabi_gate_name__, "x", [Parameter("amp")])], + calibrations=None, ) @classmethod @@ -99,26 +99,32 @@ def __init__(self, qubit: int): """ super().__init__([qubit]) - def _template_circuit(self, amp_param) -> QuantumCircuit: - """Return the template quantum circuit.""" - gate = Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[amp_param]) + def _get_parameter(self): + """Extract the parameter from the options.""" + for inst_map in self.experiment_options.instruction_name_maps: + if inst_map.free_param: + return inst_map.free_param[0] + + raise CalibrationError(f"Parameter for {self.__class__.__name__} not found.") + + def _template_circuit(self) -> Tuple[QuantumCircuit, Parameter]: + """Return the template quantum circuit and parameter.""" + param = self._get_parameter() + + gate = Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[param]) circuit = QuantumCircuit(1) circuit.append(gate, (0,)) circuit.measure_active() - return circuit + return circuit, param def _default_gate_schedule(self, backend: Optional[Backend] = None): """Create the default schedule for the Rabi gate.""" amp = Parameter("amp") - with pulse.build(backend=backend, name="rabi") as default_schedule: + with pulse.build(backend=backend, name=self.__rabi_gate_name__) as default_schedule: pulse.play( - pulse.Gaussian( - duration=self.experiment_options.duration, - amp=amp, - sigma=self.experiment_options.sigma, - ), + pulse.Gaussian(duration=160, amp=amp, sigma=40), pulse.DriveChannel(self.physical_qubits[0]), ) @@ -140,27 +146,28 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: that matches the qubit on which to run the Rabi experiment. - If the user provided schedule has more than one free parameter. """ - schedule = self.experiment_options.get("schedule", None) + circuit, param = self._template_circuit() - if schedule is None: - schedule = self._default_gate_schedule(backend=backend) + # Inject the calibrations if present. + if self.experiment_options.calibrations is not None: + circuit = inject_calibrations(circuit, self) else: - if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels): - raise CalibrationError( - f"User provided schedule {schedule.name} does not contain a channel " - "for the qubit on which to run Rabi." - ) + schedule = self.experiment_options.get("schedule", self._default_gate_schedule(backend)) - if len(schedule.parameters) != 1: - raise CalibrationError("Schedule in Rabi must have exactly one free parameter.") + circuit.add_calibration( + self.__rabi_gate_name__, (self.physical_qubits[0],), schedule, params=[param] + ) - param = next(iter(schedule.parameters)) + # Sanity check the schedule in the circuit. + schedule = next(iter(circuit.calibrations[self.__rabi_gate_name__].values())) + if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels): + raise CalibrationError( + f"User provided schedule {schedule.name} does not contain a channel " + "for the qubit on which to run Rabi." + ) - # Create template circuit - circuit = self._template_circuit(param) - circuit.add_calibration( - self.__rabi_gate_name__, (self.physical_qubits[0],), schedule, params=[param] - ) + if len(schedule.parameters) != 1: + raise CalibrationError("Schedule in Rabi must have exactly one free parameter.") # Create the circuits to run circs = [] @@ -222,8 +229,6 @@ def _default_experiment_options(cls) -> Options: schedule=None, normalization=True, frequency_shift=None, - x_schedule_name="x", - calibrations=None, ) def _default_gate_schedule(self, backend: Optional[Backend] = None): @@ -265,19 +270,13 @@ def _default_gate_schedule(self, backend: Optional[Backend] = None): return default_schedule - def _template_circuit(self, amp_param) -> QuantumCircuit: + def _template_circuit(self) -> Tuple[QuantumCircuit, Parameter]: """Return the template quantum circuit.""" + param = self._get_parameter() + circuit = QuantumCircuit(1) circuit.x(0) - circuit.append(Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[amp_param]), (0,)) + circuit.append(Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[param]), (0,)) circuit.measure_active() - if self.experiment_options.calibrations is not None: - cals = self.experiment_options.calibrations - gate_schedule_map = {"x": self.experiment_options.x_schedule_name} - qubit_layout = {0: self.physical_qubits[0]} - cal_adder = PassManager(CalibrationAdder(cals, gate_schedule_map, qubit_layout)) - - circuit = cal_adder.run(circuit) - - return circuit + return circuit, param From 4fb1292cb1d8b41507ac237aa124b90ae78a5971 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Jul 2021 08:19:15 +0200 Subject: [PATCH 6/9] * Improved docs. --- .../calibration_management/transpiler.py | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/calibration_management/transpiler.py b/qiskit_experiments/calibration_management/transpiler.py index ce9117c1b7..191f8a0729 100644 --- a/qiskit_experiments/calibration_management/transpiler.py +++ b/qiskit_experiments/calibration_management/transpiler.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Transpiler functionality for calibrations.""" +"""Transpiler pass for calibration experiments.""" from typing import Dict, List, Optional, Tuple, Union @@ -27,7 +27,7 @@ class CalAdder(TransformationPass): - """Transformation pass to inject calibrations into circuits.""" + """Transformation pass to inject calibrations into circuits of calibration experiments.""" def __init__( self, @@ -35,10 +35,39 @@ def __init__( instruction_maps: Optional[List[InstructionMap]] = None, qubit_layout: Optional[Dict[int, int]] = None ): - """ + """Initialize the pass. This transpiler pass is intended to be run in the :meth:`circuits` method of the - experiment classes before the main transpiler pass. + experiment classes before the main transpiler pass. It's only goal is to extract + the needed pulse schedules from an instance of Calibrations and attach them to the + template circuit. This has a couple of challenges. + + * First, the same pulse in the calibrations can be attached to different gates. + For example, an X-gate "x" may need to be attached to a Rabi gate in a + :class:`Rabi` experiment while in an :class:`EFSpectroscopy` experiment it will + be attached to the X-gate. + + * Second, the gate may sometimes be attached with parameters and sometimes not. + In a Rabi experiment the "x" schedule will have a parametric amplitude while in + the :class:`FineXAmplitude` the gate will not have any free parameters. + + These two issues are solved by adding an InstructionMap which is a named tuple of + instruction name in the circuit, the schedule name in the calibrations and any + parameter instance that needs to be unassigned when getting the schedule from the + :class:`Calibrations` instance. Consider the following examples. + + .. code-block::python + + # Instruction mapping for a Rabi experiment + InstructionMap("Rabi", "x", [Parameter("amp")]) + + # Instruction mapping for a Drag experiment + beta = Parameter("β") + InstructionMap("Rp", "x", [beta]) + InstructionMap("Rm", "xm", [beta]) + + Note that if no mapping is provided this transpiler pass assumes that the name of + the schedule in the calibrations is the same as the name of the gate instruction. Args: calibrations: An instance of calibration from which to fetch the schedules. @@ -55,7 +84,7 @@ def __init__( self._instruction_maps = dict() for inst_map in instruction_maps: - self._intruction_maps[inst_map.inst] = inst_map + self._instruction_maps[inst_map.inst] = inst_map self._qubit_layout = qubit_layout @@ -64,7 +93,7 @@ def get_calibration( gate_name: str, qubits: Tuple[int, ...], ) -> Union[ScheduleBlock, None]: - """Gets the calibrated schedule + """Get a schedule from the calibrations. Args: gate_name: Name of the gate for which to get the schedule. @@ -121,7 +150,7 @@ def inject_calibrations(circuit: QuantumCircuit, experiment: BaseExperiment) -> """Inject calibrations from a :class:`Calibrations` instance into a circuit. This function requires that the experiment has a list of InstructionMaps in its - experiment options. + experiment options as well as the calibrations. Args: circuit: The circuit into which to inject calibrations. From 6e2215f7334bce85b4e64136bdeb86fccc29e53d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Jul 2021 17:17:27 +0200 Subject: [PATCH 7/9] * Added flexibility to handle Calibrations instances or user provided schedule configs. --- .../calibration_management/transpiler.py | 233 ++++++++++++------ 1 file changed, 160 insertions(+), 73 deletions(-) diff --git a/qiskit_experiments/calibration_management/transpiler.py b/qiskit_experiments/calibration_management/transpiler.py index 191f8a0729..3310be80fd 100644 --- a/qiskit_experiments/calibration_management/transpiler.py +++ b/qiskit_experiments/calibration_management/transpiler.py @@ -12,9 +12,11 @@ """Transpiler pass for calibration experiments.""" +from abc import abstractmethod from typing import Dict, List, Optional, Tuple, Union from qiskit import QuantumCircuit +from qiskit.circuit import Parameter from qiskit.dagcircuit import DAGCircuit from qiskit.pulse.schedule import ScheduleBlock from qiskit.transpiler.basepasses import TransformationPass @@ -26,48 +28,118 @@ from qiskit_experiments.framework.base_experiment import BaseExperiment -class CalAdder(TransformationPass): +class BaseCalibrationAdder(TransformationPass): """Transformation pass to inject calibrations into circuits of calibration experiments.""" - def __init__( - self, - calibrations: Calibrations, - instruction_maps: Optional[List[InstructionMap]] = None, - qubit_layout: Optional[Dict[int, int]] = None - ): + def __init__(self, qubit_layout: Optional[Dict[int, int]] = None): """Initialize the pass. - This transpiler pass is intended to be run in the :meth:`circuits` method of the - experiment classes before the main transpiler pass. It's only goal is to extract - the needed pulse schedules from an instance of Calibrations and attach them to the - template circuit. This has a couple of challenges. + Args: + qubit_layout: The initial layout to be used in the transpilation called before + running the experiment. This initial layout is needed here since the qubits + in the circuits on which this transpiler pass is run will always be [0, 1, ...]. + If the initial layout is, for example, {0: 3} then any gates on qubit 0 will + add calibrations for qubit 3. + """ + super().__init__() + self._qubit_layout = qubit_layout - * First, the same pulse in the calibrations can be attached to different gates. - For example, an X-gate "x" may need to be attached to a Rabi gate in a - :class:`Rabi` experiment while in an :class:`EFSpectroscopy` experiment it will - be attached to the X-gate. + @abstractmethod + def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[ScheduleBlock, None]: + """Get a schedule from the internally stored schedules.""" - * Second, the gate may sometimes be attached with parameters and sometimes not. - In a Rabi experiment the "x" schedule will have a parametric amplitude while in - the :class:`FineXAmplitude` the gate will not have any free parameters. + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the calibration adder pass on `dag`. - These two issues are solved by adding an InstructionMap which is a named tuple of - instruction name in the circuit, the schedule name in the calibrations and any - parameter instance that needs to be unassigned when getting the schedule from the - :class:`Calibrations` instance. Consider the following examples. + Args: + dag: DAG to schedule. - .. code-block::python + Returns: + A DAG with calibrations added to it. + """ + bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} - # Instruction mapping for a Rabi experiment - InstructionMap("Rabi", "x", [Parameter("amp")]) + for node in dag.nodes(): + if node.type == "op": + params = node.op.params + + # Get the qubit indices in the circuit. + qubits = tuple(bit_indices[qarg] for qarg in node.qargs) + + # Get the physical qubits that they will remap to. + qubits = tuple(self._qubit_layout[qubit] for qubit in qubits) + + schedule = self._get_calibration(node.op.name, qubits) + + # Permissive stance: if we don't find a schedule we continue. + # The call to the transpiler that happens before running the + # experiment will either complain or force us to use the + # backend gates. + if schedule is None: + continue + + if len(set(qubits) & set(ch.index for ch in schedule.channels)) == 0: + raise CalibrationError( + f"None of the qubits {qubits} are contained in the channels of " + f"the schedule named {schedule.name} for gate {node.op}." + ) + + # Check the consistency between the circuit and schedule parameters + for param in params: + if isinstance(param, Parameter) and param not in schedule.parameters: + raise CalibrationError( + f"Gate {node.op.name} has parameter {param} that is not contained " + f"in schedule {schedule}." + ) + + dag.add_calibration(node.op, qubits, schedule, params=params) + + return dag - # Instruction mapping for a Drag experiment - beta = Parameter("β") - InstructionMap("Rp", "x", [beta]) - InstructionMap("Rm", "xm", [beta]) - Note that if no mapping is provided this transpiler pass assumes that the name of - the schedule in the calibrations is the same as the name of the gate instruction. +class CalibrationsAdder(BaseCalibrationAdder): + """This BaseCalibrationAdder stores schedules in the calibrations. + + This transpiler pass is intended to be run in the :meth:`circuits` method of the + experiment classes before the main transpiler pass. It's only goal is to extract + the needed pulse schedules from an instance of Calibrations and attach them to the + template circuit. This has a couple of challenges. + + * First, the same pulse in the calibrations can be attached to different gates. + For example, an X-gate "x" may need to be attached to a Rabi gate in a + :class:`Rabi` experiment while in an :class:`EFSpectroscopy` experiment it will + be attached to the X-gate. + + * Second, the gate may sometimes be attached with parameters and sometimes not. + In a Rabi experiment the "x" schedule will have a parametric amplitude while in + the :class:`FineXAmplitude` the gate will not have any free parameters. + + These two issues are solved by adding an InstructionMap which is a named tuple of + instruction name in the circuit, the schedule name in the calibrations and any + parameter instance that needs to be unassigned when getting the schedule from the + :class:`Calibrations` instance. Consider the following examples. + + .. code-block::python + + # Instruction mapping for a Rabi experiment + InstructionMap("Rabi", "x", [Parameter("amp")]) + + # Instruction mapping for a Drag experiment + beta = Parameter("β") + InstructionMap("Rp", "x", [beta]) + InstructionMap("Rm", "xm", [beta]) + + Note that if no mapping is provided this transpiler pass assumes that the name of + the schedule in the calibrations is the same as the name of the gate instruction. + """ + + def __init__( + self, + calibrations: Calibrations, + instruction_maps: Optional[List[InstructionMap]] = None, + qubit_layout: Optional[Dict[int, int]] = None, + ): + """Initialize the pass. Args: calibrations: An instance of calibration from which to fetch the schedules. @@ -75,39 +147,32 @@ def __init__( schedule names in the calibrations. If this is not provided the transpiler pass will assume that the schedule has the same name as the gate. Each instruction map may also specify parameters that should be left free in the schedule. - qubit_layout: The initial layout that will be used. This remaps the qubits - in the added calibrations. For instance, if {0: 3} is given and use this pass - on a circuit then any gates on qubit 0 will add calibrations for qubit 3. + qubit_layout: The initial layout that will be used. """ - super().__init__() + super().__init__(qubit_layout) self._cals = calibrations self._instruction_maps = dict() for inst_map in instruction_maps: self._instruction_maps[inst_map.inst] = inst_map - self._qubit_layout = qubit_layout - def get_calibration( - self, - gate_name: str, - qubits: Tuple[int, ...], - ) -> Union[ScheduleBlock, None]: - """Get a schedule from the calibrations. + def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[ScheduleBlock, None]: + """Get a schedule from the internally stored calibrations. Args: - gate_name: Name of the gate for which to get the schedule. + gate: Name of the gate for which to get the schedule. qubits: The qubits for which to get the parameters. Returns: The schedule if one is found otherwise return None. """ - name = gate_name + name = gate assign_params = None # check for a non-trivial instruction to schedule mapping. - if gate_name in self._instruction_maps: - inst_map = self._instruction_maps[gate_name] + if gate in self._instruction_maps: + inst_map = self._instruction_maps[gate] name = inst_map.schedule assign_params = {param.name: param for param in inst_map.free_params} @@ -116,51 +181,73 @@ def get_calibration( except CalibrationError: return None - def run(self, dag: DAGCircuit) -> DAGCircuit: - """Run the calibration adder pass on `dag`. - Args: - dag: DAG to schedule. +class ScheduleAdder(BaseCalibrationAdder): + """A naive calibrations adder for lists of schedules. - Returns: - A DAG with calibrations added to it. - """ - bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + This calibration adder is used for cases when users provide schedules for the + experiment as a dict of schedules. + """ - for node in dag.nodes(): - if node.type == "op": - params = node.op.params - qubits = tuple(bit_indices[qarg] for qarg in node.qargs) + def __init__( + self, + schedules: Dict[Tuple[str, Tuple[int, ...]], ScheduleBlock], + qubit_layout: Optional[Dict[int, int]] = None, + ): + """Initialize the :class:`ScheduleAdder` from a dict of schedules. - if self._qubit_layout is not None: - try: - qubits = tuple(self._qubit_layout[qubit] for qubit in qubits) - except KeyError: - pass + Args: + schedules: The schedules are provided as a dict. Here, the keys correspond to a + tuple of the name of the instruction in the quantum circuits and the qubits + while the values are the schedules that will be added to the calibrations of + the QuantumCircuit. + qubit_layout: The initial layout that will be used. + """ + super().__init__(qubit_layout) + self._schedules = schedules - schedule = self.get_calibration(node.op.name, qubits) + def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[ScheduleBlock, None]: + """Get a schedule from the internally stored schedules. - if schedule is not None: - dag.add_calibration(node.op, qubits, schedule, params=params) + Args: + gate: Name of the gate for which to get the schedule. + qubits: The qubits for which to get the parameters. - return dag + Returns: + The schedule if one is found otherwise return None. + """ + return self._schedules.get((gate, qubits), None) -def inject_calibrations(circuit: QuantumCircuit, experiment: BaseExperiment) -> QuantumCircuit: +def inject_calibrations( + circuits: Union[QuantumCircuit, List[QuantumCircuit]], + experiment: BaseExperiment +) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Inject calibrations from a :class:`Calibrations` instance into a circuit. This function requires that the experiment has a list of InstructionMaps in its experiment options as well as the calibrations. Args: - circuit: The circuit into which to inject calibrations. + circuits: The circuit or list of circuits into which to inject calibrations. experiment: The experiment object that Returns: A quantum circuit with the relevant calibrations inject into it. """ - calibrations = experiment.experiment_options.calibrations - inst_maps = experiment.experiment_options.instruction_name_maps - layout = {idx: qubit for idx, qubit in enumerate(experiment.physical_qubits)} + layout = {idx: qubit for idx, qubit in enumerate(experiment.physical_qubits)} + + calibrations = experiment.experiment_options.get("calibrations", None) + + if calibrations is None: + user_schedule_config = experiment.experiment_options.get("schedules", None) + + if user_schedule_config is None: + return circuits + + return PassManager(ScheduleAdder(user_schedule_config, layout)).run(circuits) + + else: + inst_maps = experiment.experiment_options.instruction_name_maps - return PassManager(CalAdder(calibrations, inst_maps, layout)).run(circuit) + return PassManager(CalibrationsAdder(calibrations, inst_maps, layout)).run(circuits) From 3691b4b86171230d0f30bca3815c42946591609a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Jul 2021 21:26:43 +0200 Subject: [PATCH 8/9] * Propagating changes to the Rabi. --- .../calibration_key_types.py | 1 - .../calibration_management/transpiler.py | 75 +++++++++++++------ .../library/calibration/fine_amplitude.py | 6 ++ .../library/calibration/rabi.py | 47 +++++------- test/calibration/experiments/test_rabi.py | 4 +- 5 files changed, 79 insertions(+), 54 deletions(-) diff --git a/qiskit_experiments/calibration_management/calibration_key_types.py b/qiskit_experiments/calibration_management/calibration_key_types.py index 91d88a7dc4..a1bdcccbc5 100644 --- a/qiskit_experiments/calibration_management/calibration_key_types.py +++ b/qiskit_experiments/calibration_management/calibration_key_types.py @@ -20,5 +20,4 @@ ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) ScheduleKey = namedtuple("ScheduleKey", ["schedule", "qubits"]) -InstructionMap = namedtuple("InstructionMap", ["inst", "schedule", "free_params"]) ParameterValueType = Union[ParameterExpression, float, int, complex] diff --git a/qiskit_experiments/calibration_management/transpiler.py b/qiskit_experiments/calibration_management/transpiler.py index 3310be80fd..12cd30ec18 100644 --- a/qiskit_experiments/calibration_management/transpiler.py +++ b/qiskit_experiments/calibration_management/transpiler.py @@ -22,12 +22,35 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passmanager import PassManager -from qiskit_experiments.calibration.management.calibrations import Calibrations +from qiskit_experiments.calibration_management.calibrations import Calibrations from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management.calibration_key_types import InstructionMap from qiskit_experiments.framework.base_experiment import BaseExperiment +class CalibrationsMap: + + def __init__(self): + """""" + self._map = dict() + + def add(self, gate_name: str, schedule_name: str, parameter_map: Dict[str, str]): + """ + Args: + gate_name: + schedule_name: + parameter_map: + """ + self._map[gate_name] = (schedule_name, parameter_map) + + def get(self, gate_name: str) -> Tuple[str, Dict]: + """""" + if gate_name in self._map: + return self._map[gate_name] + + # Return the trivial map + return gate_name, {} + + class BaseCalibrationAdder(TransformationPass): """Transformation pass to inject calibrations into circuits of calibration experiments.""" @@ -45,7 +68,9 @@ def __init__(self, qubit_layout: Optional[Dict[int, int]] = None): self._qubit_layout = qubit_layout @abstractmethod - def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[ScheduleBlock, None]: + def _get_calibration( + self, gate: str, qubits: Tuple[int, ...], params: List[Parameter] + ) -> Union[ScheduleBlock, None]: """Get a schedule from the internally stored schedules.""" def run(self, dag: DAGCircuit) -> DAGCircuit: @@ -69,7 +94,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # Get the physical qubits that they will remap to. qubits = tuple(self._qubit_layout[qubit] for qubit in qubits) - schedule = self._get_calibration(node.op.name, qubits) + schedule = self._get_calibration(node.op.name, qubits, params) # Permissive stance: if we don't find a schedule we continue. # The call to the transpiler that happens before running the @@ -136,14 +161,14 @@ class CalibrationsAdder(BaseCalibrationAdder): def __init__( self, calibrations: Calibrations, - instruction_maps: Optional[List[InstructionMap]] = None, + calibrations_map: Optional[CalibrationsMap] = None, qubit_layout: Optional[Dict[int, int]] = None, ): """Initialize the pass. Args: calibrations: An instance of calibration from which to fetch the schedules. - instruction_maps: A list of instruction maps to map gate names in the circuit to + calibrations_map: A list of instruction maps to map gate names in the circuit to schedule names in the calibrations. If this is not provided the transpiler pass will assume that the schedule has the same name as the gate. Each instruction map may also specify parameters that should be left free in the schedule. @@ -151,13 +176,13 @@ def __init__( """ super().__init__(qubit_layout) self._cals = calibrations + self._calibrations_map = calibrations_map or dict() - self._instruction_maps = dict() - for inst_map in instruction_maps: - self._instruction_maps[inst_map.inst] = inst_map - - - def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[ScheduleBlock, None]: + def _get_calibration( + self, gate: str, + qubits: Tuple[int, ...], + node_params: List[Parameter] + ) -> Union[ScheduleBlock, None]: """Get a schedule from the internally stored calibrations. Args: @@ -167,17 +192,18 @@ def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[Schedule Returns: The schedule if one is found otherwise return None. """ - name = gate - assign_params = None - # check for a non-trivial instruction to schedule mapping. - if gate in self._instruction_maps: - inst_map = self._instruction_maps[gate] - name = inst_map.schedule - assign_params = {param.name: param for param in inst_map.free_params} + # Extract the gate to schedule and any parameter name mappings. + sched_name, params_map = self._calibrations_map.get(gate) + + assign_params = {} + for param in node_params: + if isinstance(param, Parameter): + assign_params[params_map.get(param.name, param.name)] = param + # Try and get a schedule, if there is none then return None. try: - return self._cals.get_schedule(name, qubits, assign_params=assign_params) + return self._cals.get_schedule(sched_name, qubits, assign_params=assign_params) except CalibrationError: return None @@ -206,7 +232,12 @@ def __init__( super().__init__(qubit_layout) self._schedules = schedules - def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[ScheduleBlock, None]: + def _get_calibration( + self, + gate: str, + qubits: Tuple[int, ...], + node_params: List[Parameter] + ) -> Union[ScheduleBlock, None]: """Get a schedule from the internally stored schedules. Args: @@ -240,7 +271,7 @@ def inject_calibrations( calibrations = experiment.experiment_options.get("calibrations", None) if calibrations is None: - user_schedule_config = experiment.experiment_options.get("schedules", None) + user_schedule_config = experiment.experiment_options.get("schedules_config", None) if user_schedule_config is None: return circuits diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py index 3012e3517c..b6d6894ba2 100644 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ b/qiskit_experiments/library/calibration/fine_amplitude.py @@ -27,6 +27,7 @@ FineAmplitudeAnalysis, ) from qiskit_experiments.exceptions import CalibrationError +from qiskit_experiments.calibration_management.transpiler import inject_calibrations class FineAmplitude(BaseExperiment): @@ -122,6 +123,7 @@ def _default_experiment_options(cls) -> Options: options.add_sx = False options.add_xp_circuit = True options.sx_schedule = None + options.calibrations = None return options @@ -205,6 +207,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: if schedule is None: raise CalibrationError("No schedule set for fine amplitude calibration.") + # TODO Get ride of this? if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels): raise CalibrationError( f"User provided schedule {schedule.name} does not contain a channel " @@ -269,6 +272,9 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits.append(circuit) + if self.experiment_options.calibrations is not None: + circuits = inject_calibrations(circuits, self) + return circuits diff --git a/qiskit_experiments/library/calibration/rabi.py b/qiskit_experiments/library/calibration/rabi.py index 9ac0b24ced..7ed1a8f144 100644 --- a/qiskit_experiments/library/calibration/rabi.py +++ b/qiskit_experiments/library/calibration/rabi.py @@ -25,8 +25,8 @@ from qiskit_experiments.framework import BaseExperiment from qiskit_experiments.library.calibration.analysis.oscillation_analysis import OscillationAnalysis from qiskit_experiments.exceptions import CalibrationError +from qiskit_experiments.calibration_management.transpiler import CalibrationsMap from qiskit_experiments.calibration_management.transpiler import inject_calibrations -from qiskit_experiments.calibration_management.calibration_key_types import InstructionMap class Rabi(BaseExperiment): @@ -49,6 +49,7 @@ class Rabi(BaseExperiment): __analysis_class__ = OscillationAnalysis __rabi_gate_name__ = "Rabi" + __rabi_param_name__ = "amp" @classmethod def _default_run_options(cls) -> Options: @@ -69,10 +70,13 @@ def _default_experiment_options(cls) -> Options: rabi.set_experiment_options(schedule=rabi_schedule) """ + cal_map = CalibrationsMap() + cal_map.add(cls.__rabi_gate_name__, "x", {cls.__rabi_param_name__: "amp"}) + return Options( amplitudes=np.linspace(-0.95, 0.95, 51), - schedule=None, - instruction_name_maps=[InstructionMap(cls.__rabi_gate_name__, "x", [Parameter("amp")])], + schedules_config=None, + instruction_name_maps=cal_map, calibrations=None, ) @@ -107,17 +111,16 @@ def _get_parameter(self): raise CalibrationError(f"Parameter for {self.__class__.__name__} not found.") - def _template_circuit(self) -> Tuple[QuantumCircuit, Parameter]: - """Return the template quantum circuit and parameter.""" - param = self._get_parameter() + def _template_circuit(self) -> QuantumCircuit: + """Return the template quantum circuit.""" - gate = Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[param]) + gate = Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[Parameter("amp")]) circuit = QuantumCircuit(1) circuit.append(gate, (0,)) circuit.measure_active() - return circuit, param + return circuit def _default_gate_schedule(self, backend: Optional[Backend] = None): """Create the default schedule for the Rabi gate.""" @@ -146,26 +149,13 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: that matches the qubit on which to run the Rabi experiment. - If the user provided schedule has more than one free parameter. """ - circuit, param = self._template_circuit() + circuit = self._template_circuit() - # Inject the calibrations if present. - if self.experiment_options.calibrations is not None: - circuit = inject_calibrations(circuit, self) - else: - schedule = self.experiment_options.get("schedule", self._default_gate_schedule(backend)) + # Inject calibrations or user-provided schedules if present. + circuit = inject_calibrations(circuit, self) - circuit.add_calibration( - self.__rabi_gate_name__, (self.physical_qubits[0],), schedule, params=[param] - ) - - # Sanity check the schedule in the circuit. + # Sanity check the schedule for Rabi. schedule = next(iter(circuit.calibrations[self.__rabi_gate_name__].values())) - if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels): - raise CalibrationError( - f"User provided schedule {schedule.name} does not contain a channel " - "for the qubit on which to run Rabi." - ) - if len(schedule.parameters) != 1: raise CalibrationError("Schedule in Rabi must have exactly one free parameter.") @@ -270,13 +260,12 @@ def _default_gate_schedule(self, backend: Optional[Backend] = None): return default_schedule - def _template_circuit(self) -> Tuple[QuantumCircuit, Parameter]: + def _template_circuit(self) -> QuantumCircuit: """Return the template quantum circuit.""" - param = self._get_parameter() circuit = QuantumCircuit(1) circuit.x(0) - circuit.append(Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[param]), (0,)) + circuit.append(Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[Parameter("amp")]), (0,)) circuit.measure_active() - return circuit, param + return circuit diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index ced50543f8..a2942e11b5 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -26,8 +26,8 @@ from qiskit_experiments.framework import ExperimentData, ParallelExperiment from qiskit_experiments.library import Rabi, EFRabi -from qiskit_experiments.calibration.analysis.oscillation_analysis import OscillationAnalysis -from qiskit_experiments.calibration.management.calibrations import Calibrations +from qiskit_experiments.library.calibration.analysis.oscillation_analysis import OscillationAnalysis +from qiskit_experiments.calibration_management.calibrations import Calibrations from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.nodes import Probability from qiskit_experiments.test.mock_iq_backend import MockIQBackend From 28c22802142c4ae370f7bce2e82b21c809dd1ccc Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 27 Jul 2021 11:45:56 +0200 Subject: [PATCH 9/9] * Revert to previous logic. --- .../calibration_management/transpiler.py | 244 ++++-------------- .../library/calibration/fine_amplitude.py | 3 +- .../library/calibration/rabi.py | 100 ++++--- test/calibration/experiments/test_rabi.py | 7 +- 4 files changed, 106 insertions(+), 248 deletions(-) diff --git a/qiskit_experiments/calibration_management/transpiler.py b/qiskit_experiments/calibration_management/transpiler.py index 12cd30ec18..064d26cf13 100644 --- a/qiskit_experiments/calibration_management/transpiler.py +++ b/qiskit_experiments/calibration_management/transpiler.py @@ -12,11 +12,9 @@ """Transpiler pass for calibration experiments.""" -from abc import abstractmethod -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Tuple, Union from qiskit import QuantumCircuit -from qiskit.circuit import Parameter from qiskit.dagcircuit import DAGCircuit from qiskit.pulse.schedule import ScheduleBlock from qiskit.transpiler.basepasses import TransformationPass @@ -27,51 +25,56 @@ from qiskit_experiments.framework.base_experiment import BaseExperiment -class CalibrationsMap: +class CalibrationsAdder(TransformationPass): + """A transformation pass to add calibrations for standard circuit instructions. - def __init__(self): - """""" - self._map = dict() - - def add(self, gate_name: str, schedule_name: str, parameter_map: Dict[str, str]): - """ - Args: - gate_name: - schedule_name: - parameter_map: - """ - self._map[gate_name] = (schedule_name, parameter_map) - - def get(self, gate_name: str) -> Tuple[str, Dict]: - """""" - if gate_name in self._map: - return self._map[gate_name] - - # Return the trivial map - return gate_name, {} - - -class BaseCalibrationAdder(TransformationPass): - """Transformation pass to inject calibrations into circuits of calibration experiments.""" + This transpiler pass is intended to be run in the :meth:`circuits` method of the + experiment classes before the main transpiler pass. It's only goal is to extract + the needed pulse schedules from an instance of Calibrations and attach them to the + template circuit. This has a couple of challenges. Note that if no mapping is provided + this transpiler pass assumes that the name of the schedule in the calibrations is the + same as the name of the gate instruction. + """ - def __init__(self, qubit_layout: Optional[Dict[int, int]] = None): + def __init__( + self, + calibrations: Calibrations, + instructions_map: Dict[str, str], + qubit_layout: Dict[int, int], + ): """Initialize the pass. Args: - qubit_layout: The initial layout to be used in the transpilation called before - running the experiment. This initial layout is needed here since the qubits - in the circuits on which this transpiler pass is run will always be [0, 1, ...]. - If the initial layout is, for example, {0: 3} then any gates on qubit 0 will - add calibrations for qubit 3. + calibrations: An instance of calibration from which to fetch the schedules. + instructions_map: A map of circuit instruction names (keys) to schedule names stored + in the calibrations (values). If an entry is not found the pass will assume that + the instruction in the circuit and the schedule have the same name. + qubit_layout: The initial layout that will be used. """ super().__init__() self._qubit_layout = qubit_layout + self._cals = calibrations + self._instructions_map = instructions_map - @abstractmethod - def _get_calibration( - self, gate: str, qubits: Tuple[int, ...], params: List[Parameter] - ) -> Union[ScheduleBlock, None]: - """Get a schedule from the internally stored schedules.""" + def _get_calibration(self, gate: str, qubits: Tuple[int, ...]) -> Union[ScheduleBlock, None]: + """Get a schedule from the internally stored calibrations. + + Args: + gate: Name of the gate for which to get the schedule. + qubits: The qubits for which to get the parameters. + + Returns: + The schedule if one is found otherwise return None. + """ + + # Extract the gate to schedule and any parameter name mappings. + sched_name = self._instructions_map.get(gate, gate) + + # Try and get a schedule, if there is none then return None. + try: + return self._cals.get_schedule(sched_name, qubits) + except CalibrationError: + return None def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the calibration adder pass on `dag`. @@ -86,7 +89,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: for node in dag.nodes(): if node.type == "op": - params = node.op.params # Get the qubit indices in the circuit. qubits = tuple(bit_indices[qarg] for qarg in node.qargs) @@ -94,7 +96,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # Get the physical qubits that they will remap to. qubits = tuple(self._qubit_layout[qubit] for qubit in qubits) - schedule = self._get_calibration(node.op.name, qubits, params) + schedule = self._get_calibration(node.op.name, qubits) # Permissive stance: if we don't find a schedule we continue. # The call to the transpiler that happens before running the @@ -109,176 +111,34 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: f"the schedule named {schedule.name} for gate {node.op}." ) - # Check the consistency between the circuit and schedule parameters - for param in params: - if isinstance(param, Parameter) and param not in schedule.parameters: - raise CalibrationError( - f"Gate {node.op.name} has parameter {param} that is not contained " - f"in schedule {schedule}." - ) - - dag.add_calibration(node.op, qubits, schedule, params=params) + dag.add_calibration(node.op, qubits, schedule) return dag -class CalibrationsAdder(BaseCalibrationAdder): - """This BaseCalibrationAdder stores schedules in the calibrations. - - This transpiler pass is intended to be run in the :meth:`circuits` method of the - experiment classes before the main transpiler pass. It's only goal is to extract - the needed pulse schedules from an instance of Calibrations and attach them to the - template circuit. This has a couple of challenges. - - * First, the same pulse in the calibrations can be attached to different gates. - For example, an X-gate "x" may need to be attached to a Rabi gate in a - :class:`Rabi` experiment while in an :class:`EFSpectroscopy` experiment it will - be attached to the X-gate. - - * Second, the gate may sometimes be attached with parameters and sometimes not. - In a Rabi experiment the "x" schedule will have a parametric amplitude while in - the :class:`FineXAmplitude` the gate will not have any free parameters. - - These two issues are solved by adding an InstructionMap which is a named tuple of - instruction name in the circuit, the schedule name in the calibrations and any - parameter instance that needs to be unassigned when getting the schedule from the - :class:`Calibrations` instance. Consider the following examples. - - .. code-block::python - - # Instruction mapping for a Rabi experiment - InstructionMap("Rabi", "x", [Parameter("amp")]) - - # Instruction mapping for a Drag experiment - beta = Parameter("β") - InstructionMap("Rp", "x", [beta]) - InstructionMap("Rm", "xm", [beta]) - - Note that if no mapping is provided this transpiler pass assumes that the name of - the schedule in the calibrations is the same as the name of the gate instruction. - """ - - def __init__( - self, - calibrations: Calibrations, - calibrations_map: Optional[CalibrationsMap] = None, - qubit_layout: Optional[Dict[int, int]] = None, - ): - """Initialize the pass. - - Args: - calibrations: An instance of calibration from which to fetch the schedules. - calibrations_map: A list of instruction maps to map gate names in the circuit to - schedule names in the calibrations. If this is not provided the transpiler pass - will assume that the schedule has the same name as the gate. Each instruction map - may also specify parameters that should be left free in the schedule. - qubit_layout: The initial layout that will be used. - """ - super().__init__(qubit_layout) - self._cals = calibrations - self._calibrations_map = calibrations_map or dict() - - def _get_calibration( - self, gate: str, - qubits: Tuple[int, ...], - node_params: List[Parameter] - ) -> Union[ScheduleBlock, None]: - """Get a schedule from the internally stored calibrations. - - Args: - gate: Name of the gate for which to get the schedule. - qubits: The qubits for which to get the parameters. - - Returns: - The schedule if one is found otherwise return None. - """ - - # Extract the gate to schedule and any parameter name mappings. - sched_name, params_map = self._calibrations_map.get(gate) - - assign_params = {} - for param in node_params: - if isinstance(param, Parameter): - assign_params[params_map.get(param.name, param.name)] = param - - # Try and get a schedule, if there is none then return None. - try: - return self._cals.get_schedule(sched_name, qubits, assign_params=assign_params) - except CalibrationError: - return None - - -class ScheduleAdder(BaseCalibrationAdder): - """A naive calibrations adder for lists of schedules. - - This calibration adder is used for cases when users provide schedules for the - experiment as a dict of schedules. - """ - - def __init__( - self, - schedules: Dict[Tuple[str, Tuple[int, ...]], ScheduleBlock], - qubit_layout: Optional[Dict[int, int]] = None, - ): - """Initialize the :class:`ScheduleAdder` from a dict of schedules. - - Args: - schedules: The schedules are provided as a dict. Here, the keys correspond to a - tuple of the name of the instruction in the quantum circuits and the qubits - while the values are the schedules that will be added to the calibrations of - the QuantumCircuit. - qubit_layout: The initial layout that will be used. - """ - super().__init__(qubit_layout) - self._schedules = schedules - - def _get_calibration( - self, - gate: str, - qubits: Tuple[int, ...], - node_params: List[Parameter] - ) -> Union[ScheduleBlock, None]: - """Get a schedule from the internally stored schedules. - - Args: - gate: Name of the gate for which to get the schedule. - qubits: The qubits for which to get the parameters. - - Returns: - The schedule if one is found otherwise return None. - """ - return self._schedules.get((gate, qubits), None) - - def inject_calibrations( circuits: Union[QuantumCircuit, List[QuantumCircuit]], experiment: BaseExperiment ) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Inject calibrations from a :class:`Calibrations` instance into a circuit. - This function requires that the experiment has a list of InstructionMaps in its - experiment options as well as the calibrations. + This function only adds calibrations if it can find them in the calibrations. Args: circuits: The circuit or list of circuits into which to inject calibrations. experiment: The experiment object that Returns: - A quantum circuit with the relevant calibrations inject into it. + A quantum circuit with the relevant calibrations added to it. """ layout = {idx: qubit for idx, qubit in enumerate(experiment.physical_qubits)} + # Identify the available schedule data. calibrations = experiment.experiment_options.get("calibrations", None) - if calibrations is None: - user_schedule_config = experiment.experiment_options.get("schedules_config", None) - - if user_schedule_config is None: - return circuits - - return PassManager(ScheduleAdder(user_schedule_config, layout)).run(circuits) - - else: - inst_maps = experiment.experiment_options.instruction_name_maps - + # Run the transpiler pass according to the available data + if calibrations is not None: + inst_maps = experiment.experiment_options.get("instruction_name_map", None) or dict() return PassManager(CalibrationsAdder(calibrations, inst_maps, layout)).run(circuits) + + return circuits diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py index b6d6894ba2..4a4ee8f513 100644 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ b/qiskit_experiments/library/calibration/fine_amplitude.py @@ -272,8 +272,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits.append(circuit) - if self.experiment_options.calibrations is not None: - circuits = inject_calibrations(circuits, self) + circuits = inject_calibrations(circuits, self) return circuits diff --git a/qiskit_experiments/library/calibration/rabi.py b/qiskit_experiments/library/calibration/rabi.py index 7ed1a8f144..7ae7f5693c 100644 --- a/qiskit_experiments/library/calibration/rabi.py +++ b/qiskit_experiments/library/calibration/rabi.py @@ -12,7 +12,7 @@ """Rabi amplitude experiment.""" -from typing import List, Optional, Tuple +from typing import List, Optional import numpy as np from qiskit import QuantumCircuit @@ -20,12 +20,12 @@ from qiskit.qobj.utils import MeasLevel from qiskit.providers import Backend import qiskit.pulse as pulse +from qiskit.pulse import ScheduleBlock from qiskit.providers.options import Options from qiskit_experiments.framework import BaseExperiment from qiskit_experiments.library.calibration.analysis.oscillation_analysis import OscillationAnalysis from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management.transpiler import CalibrationsMap from qiskit_experiments.calibration_management.transpiler import inject_calibrations @@ -49,7 +49,6 @@ class Rabi(BaseExperiment): __analysis_class__ = OscillationAnalysis __rabi_gate_name__ = "Rabi" - __rabi_param_name__ = "amp" @classmethod def _default_run_options(cls) -> Options: @@ -70,14 +69,13 @@ def _default_experiment_options(cls) -> Options: rabi.set_experiment_options(schedule=rabi_schedule) """ - cal_map = CalibrationsMap() - cal_map.add(cls.__rabi_gate_name__, "x", {cls.__rabi_param_name__: "amp"}) - return Options( + duration=160, + sigma=40, amplitudes=np.linspace(-0.95, 0.95, 51), - schedules_config=None, - instruction_name_maps=cal_map, + schedule=None, calibrations=None, + instruction_name_map=None, ) @classmethod @@ -96,42 +94,40 @@ def __init__(self, qubit: int): - duration: The duration of the rabi pulse in samples, the default is 160 samples. - sigma: The standard deviation of the pulse, the default is duration 40. - amplitudes: The amplitude that are scanned in the experiment, default is - np.linspace(-0.95, 0.95, 51) + np.linspace(-0.95, 0.95, 51). Args: qubit: The qubit on which to run the Rabi experiment. """ super().__init__([qubit]) - def _get_parameter(self): - """Extract the parameter from the options.""" - for inst_map in self.experiment_options.instruction_name_maps: - if inst_map.free_param: - return inst_map.free_param[0] - - raise CalibrationError(f"Parameter for {self.__class__.__name__} not found.") - - def _template_circuit(self) -> QuantumCircuit: + def _template_circuit(self, sched: ScheduleBlock) -> QuantumCircuit: """Return the template quantum circuit.""" - - gate = Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[Parameter("amp")]) + gate = Gate(name=self.__rabi_gate_name__, num_qubits=1, params=list(sched.parameters)) circuit = QuantumCircuit(1) circuit.append(gate, (0,)) circuit.measure_active() + circuit.add_calibration( + self.__rabi_gate_name__, (self.physical_qubits[0],), sched, list(sched.parameters) + ) + return circuit - def _default_gate_schedule(self, backend: Optional[Backend] = None): + def default_schedules(self, backend: Optional[Backend] = None): """Create the default schedule for the Rabi gate.""" - amp = Parameter("amp") - with pulse.build(backend=backend, name=self.__rabi_gate_name__) as default_schedule: + with pulse.build(backend=backend, name=self.__rabi_gate_name__) as schedule: pulse.play( - pulse.Gaussian(duration=160, amp=amp, sigma=40), + pulse.Gaussian( + duration=self.experiment_options.duration, + amp=Parameter("amp"), + sigma=self.experiment_options.sigma, + ), pulse.DriveChannel(self.physical_qubits[0]), ) - return default_schedule + return schedule def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: """Create the circuits for the Rabi experiment. @@ -149,18 +145,30 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: that matches the qubit on which to run the Rabi experiment. - If the user provided schedule has more than one free parameter. """ - circuit = self._template_circuit() - # Inject calibrations or user-provided schedules if present. - circuit = inject_calibrations(circuit, self) + # 1. Get the schedules for the custom gates. + schedule = self.experiment_options.schedule or self.default_schedules(backend) + + if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels): + raise CalibrationError( + f"Schedule {schedule.name} does not have a channel " + f"for the qubit on which to run {self.__class__.__name__}." + ) - # Sanity check the schedule for Rabi. - schedule = next(iter(circuit.calibrations[self.__rabi_gate_name__].values())) if len(schedule.parameters) != 1: - raise CalibrationError("Schedule in Rabi must have exactly one free parameter.") + raise CalibrationError( + f"Schedule in {self.__class__.__name__} must have exactly one free parameter." + ) - # Create the circuits to run + # 2. Create template circuit and attach the calibration. + circuit = self._template_circuit(schedule) + + # 3. Inject calibrations for standard gates. + circuit = inject_calibrations(circuit, self) + + # 4. Assign parameter values to create circuits to run on. circs = [] + param = next(iter(schedule.parameters)) for amp in self.experiment_options.amplitudes: amp = np.round(amp, decimals=6) assigned_circ = circuit.assign_parameters({param: amp}, inplace=False) @@ -212,18 +220,12 @@ def _default_experiment_options(cls) -> Options: ef_rabi.set_experiment_options(schedule=rabi_schedule) """ - return Options( - duration=160, - sigma=40, - amplitudes=np.linspace(-0.95, 0.95, 51), - schedule=None, - normalization=True, - frequency_shift=None, - ) + options = super()._default_experiment_options() + options.frequency_shift=None + return options - def _default_gate_schedule(self, backend: Optional[Backend] = None): - """Create the default schedule for the EFRabi gate with a frequency shift to the 1-2 - transition.""" + def default_schedules(self, backend: Optional[Backend] = None): + """Create the default schedule with a frequency shift to the 1-2 transition.""" if self.experiment_options.frequency_shift is None: try: @@ -243,7 +245,6 @@ def _default_gate_schedule(self, backend: Optional[Backend] = None): "to be set manually through EFRabi.set_experiment_options(frequency_shift=..)." ) from att_err - amp = Parameter("amp") with pulse.build(backend=backend, name=self.__rabi_gate_name__) as default_schedule: with pulse.frequency_offset( self.experiment_options.frequency_shift, @@ -252,7 +253,7 @@ def _default_gate_schedule(self, backend: Optional[Backend] = None): pulse.play( pulse.Gaussian( duration=self.experiment_options.duration, - amp=amp, + amp=Parameter("amp"), sigma=self.experiment_options.sigma, ), pulse.DriveChannel(self.physical_qubits[0]), @@ -260,12 +261,9 @@ def _default_gate_schedule(self, backend: Optional[Backend] = None): return default_schedule - def _template_circuit(self) -> QuantumCircuit: + def _template_circuit(self, sched: ScheduleBlock) -> QuantumCircuit: """Return the template quantum circuit.""" - - circuit = QuantumCircuit(1) + circuit = QuantumCircuit(1, 1) circuit.x(0) - circuit.append(Gate(name=self.__rabi_gate_name__, num_qubits=1, params=[Parameter("amp")]), (0,)) - circuit.measure_active() - return circuit + return circuit.compose(super()._template_circuit(sched)) diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index a2942e11b5..079ee0b948 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -199,16 +199,17 @@ def test_transpile_x_gate(self): chan = Parameter("ch0") amp = Parameter("amp") - with pulse.build(name="xp") as xp_sched: + with pulse.build(name="x") as xp_sched: pulse.play(pulse.Gaussian(123, amp, 25), pulse.DriveChannel(chan)) cals.add_schedule(xp_sched) - cals.add_parameter_value(0.2, "amp", schedule="xp") + cals.add_parameter_value(0.2, "amp", schedule="x") rabi = EFRabi(qubit) - rabi.set_experiment_options(calibrations=cals, x_schedule_name="xp", frequency_shift=-330e6) + rabi.set_experiment_options(calibrations=cals, frequency_shift=-330e6) circs = rabi.circuits(RabiBackend()) + print(circs[0].calibrations) expected = xp_sched.assign_parameters({amp: 0.2, chan: 3}, inplace=False)