From c5078e56859907ff23cb6ab590194a98b0c4c2c5 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 15 Feb 2023 18:45:03 +0900 Subject: [PATCH 01/14] Update PulseGate pass to use Target internally. When inst_map is provided, it copies schedules there into target instance. This fixes a bug that custom schedules in the inst_map are ignored when transpiling circuit with V2 backend. To support this behavior, internal machinery of Target is updated so that a target instance can update itself only with inst_map without raising any error. Also InstructionProperties.calibration now only stores CalibrationEntry instances. When Schedule or ScheduleBlock are provided as a calibration, it converts schedule into CalibrationEntry instance. --- qiskit/pulse/calibration_entries.py | 44 +++++++++- .../passes/calibration/pulse_gate.py | 40 ++++++--- qiskit/transpiler/target.py | 69 ++++++++------- ...gate-pass-for-target-ebfb0ec9571f058e.yaml | 23 +++++ test/python/pulse/test_calibration_entries.py | 20 ++++- .../python/transpiler/test_pulse_gate_pass.py | 85 +++++++++++++++++++ test/python/transpiler/test_target.py | 9 +- 7 files changed, 242 insertions(+), 48 deletions(-) create mode 100644 releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py index 29f32b89f04d..80babaf88260 100644 --- a/qiskit/pulse/calibration_entries.py +++ b/qiskit/pulse/calibration_entries.py @@ -16,7 +16,7 @@ from enum import IntEnum from typing import Callable, List, Union, Optional, Sequence, Any -from qiskit.pulse.exceptions import PulseError +from qiskit.pulse.exceptions import PulseError, UnassignedDurationError from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.qobj.converters import QobjToInstructionConverter from qiskit.qobj.pulse_qobj import PulseQobjInstruction @@ -64,6 +64,15 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: """ pass + @abstractmethod + def get_duration(self) -> int: + """Get duration of schedule. + + Returns: + Integer value representing a duration of schedule in units of dt. + """ + pass + class ScheduleDef(CalibrationEntry): """In-memory Qiskit Pulse representation. @@ -146,6 +155,12 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: pass return self._definition.assign_parameters(value_dict, inplace=False) + def get_duration(self) -> int: + try: + return self._definition.duration + except UnassignedDurationError: + return None + def __eq__(self, other): # This delegates equality check to Schedule or ScheduleBlock. return self._definition == other._definition @@ -193,6 +208,10 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: schedule.metadata["publisher"] = CalibrationPublisher.QISKIT return schedule + def get_duration(self) -> int: + # Duration is undetermined until funciton is called with full arguments. + return None + def __eq__(self, other): # We cannot evaluate function equality without parsing python AST. # This simply compares wether they are the same object. @@ -256,6 +275,29 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: self._build_schedule() return super().get_schedule(*args, **kwargs) + def get_duration(self) -> int: + if self._definition: + return self._definition.duration + + # Parse Qobj instruction and find max instruction end time + t_max = 0 + for inst in self._source: + if inst.name == "parametric_pulse": + # Play instruction + duration = inst.parameters.get("duration", 0) + elif inst.name not in self._converter.get_supported_instructions(): + # Play instruction with Waveform + try: + # Get samples from the pulse library dictionary + duration = len(self._converter._pulse_library[inst.name]) + except KeyError: + continue + else: + # Acquire, Delay instruction + duration = getattr(inst, "duration", 0) + t_max = max(t_max, inst.t0 + duration) + return t_max + def __eq__(self, other): if isinstance(other, PulseQobjDef): # If both objects are Qobj just check Qobj equality. diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py index 5ed960253b62..f6f2f675b5f3 100644 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ b/qiskit/transpiler/passes/calibration/pulse_gate.py @@ -15,12 +15,10 @@ from typing import List, Union from qiskit.circuit import Instruction as CircuitInst -from qiskit.pulse import ( - Schedule, - ScheduleBlock, -) +from qiskit.pulse import Schedule, ScheduleBlock from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.transpiler.target import Target +from qiskit.transpiler.exceptions import TranspilerError from .base_builder import CalibrationBuilder @@ -59,13 +57,16 @@ def __init__( Args: inst_map: Instruction schedule map that user may override. target: The :class:`~.Target` representing the target backend, if both - ``inst_map`` and this are specified then this argument will take - precedence and ``inst_map`` will be ignored. + ``inst_map`` and this are specified then it updates instructions + in the ``target`` with ``inst_map``. """ super().__init__() - self.inst_map = inst_map - if target: - self.inst_map = target.instruction_schedule_map() + + if target is None: + target = Target() + if inst_map is not None: + target.update_from_instruction_schedule_map(inst_map) + self.target = target def supported(self, node_op: CircuitInst, qubits: List) -> bool: """Determine if a given node supports the calibration. @@ -77,7 +78,7 @@ def supported(self, node_op: CircuitInst, qubits: List) -> bool: Returns: Return ``True`` is calibration can be provided. """ - return self.inst_map.has(instruction=node_op.name, qubits=qubits) + return self.target.instruction_supported(operation_name=node_op.name, qargs=qubits) def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: """Gets the calibrated schedule for the given instruction and qubits. @@ -88,5 +89,22 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, Returns: Return Schedule of target gate instruction. + + Raises: + TranspilerError: When node is parameterized and calibration is raw schedule object. """ - return self.inst_map.get(node_op.name, qubits, *node_op.params) + inst_property = self.target[node_op.name][tuple(qubits)] + if not node_op.params: + return inst_property.calibration + try: + # CircuitInstruction doesn't preserve parameter name after parameter binding. + # Thus schedule cannot generate bind dictionary. + # Use CalibraionEntry to utilize inspected signature object. + calibration_entry = inst_property._calibration + return calibration_entry.get_schedule(*node_op.params) + except AttributeError as ex: + raise TranspilerError( + f"Calibraton for {node_op.name} of {qubits} is not a CalibraryEntry instance. " + f"Mapping from parameter values {node_op.params} to parameter objects " + f"in the schedule cannot be identified." + ) from ex diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 4a0359d5a479..3cc7e6c6d8ba 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -28,6 +28,8 @@ import rustworkx as rx from qiskit.circuit.parameter import Parameter +from qiskit.circuit.gate import Gate +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.pulse.calibration_entries import CalibrationEntry, ScheduleDef from qiskit.pulse.schedule import Schedule, ScheduleBlock @@ -423,7 +425,9 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err Args: inst_map (InstructionScheduleMap): The instruction inst_name_map (dict): An optional dictionary that maps any - instruction name in ``inst_map`` to an instruction object + instruction name in ``inst_map`` to an instruction object. + If not provided, instruction is pulled from the standard Qiskit gates, + and finally custom gate instnace is created with schedule name. error_dict (dict): A dictionary of errors of the form:: {gate_name: {qarg: error}} @@ -436,48 +440,53 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err a when updating the ``Target`` the error value will be pulled from this dictionary. If one is not found in ``error_dict`` then ``None`` will be used. - - Raises: - ValueError: If ``inst_map`` contains new instructions and - ``inst_name_map`` isn't specified - KeyError: If a ``inst_map`` contains a qarg for an instruction - that's not in the target """ - for inst in inst_map.instructions: + for inst_name in inst_map.instructions: out_props = {} - for qarg in inst_map.qubits_with_instruction(inst): - sched = inst_map.get(inst, qarg) - val = InstructionProperties(calibration=sched) + for qarg in inst_map.qubits_with_instruction(inst_name): try: qarg = tuple(qarg) except TypeError: qarg = (qarg,) - if inst in self._gate_map: - if self.dt is not None: - val.duration = sched.duration * self.dt - else: - val.duration = None - if error_dict is not None: - error_inst = error_dict.get(inst) - if error_inst: - error = error_inst.get(qarg) - val.error = error - else: - val.error = None + entry = inst_map._get_calibration_entry(inst_name, qarg) + val = InstructionProperties(calibration=entry) + if self.dt is not None: + val.duration = entry.get_duration() * self.dt + else: + val.duration = None + if inst_name in self._gate_map and error_dict is not None: + error_inst = error_dict.get(inst_name) + if error_inst: + error = error_inst.get(qarg) + val.error = error else: val.error = None + else: + val.error = None out_props[qarg] = val - if inst not in self._gate_map: - if inst_name_map is not None: - self.add_instruction(inst_name_map[inst], out_props, name=inst) + if inst_name not in self._gate_map: + if inst_name_map is None: + inst_name_map = get_standard_gate_name_mapping() + if inst_name in inst_name_map: + inst_obj = inst_name_map[inst_name] else: - raise ValueError( - "An inst_name_map kwarg must be specified to add new " - "instructions from an InstructionScheduleMap" + # Custom gate object, which doesn't belong to standard Qiskit gates. + inst_obj = Gate( + name=inst_name, + num_qubits=len(qarg), + params=list(map(Parameter, entry.get_signature().parameters.keys())), ) + normalized_props = {} + for qargs, prop in out_props.items(): + if len(qargs) != inst_obj.num_qubits: + continue + normalized_props[qargs] = prop + self.add_instruction(inst_obj, normalized_props, name=inst_name) else: for qarg, prop in out_props.items(): - self.update_instruction_properties(inst, qarg, prop) + if qarg not in self._gate_map[inst_name]: + continue + self.update_instruction_properties(inst_name, qarg, prop) @property def qargs(self): diff --git a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml new file mode 100644 index 000000000000..8281f85ad6b1 --- /dev/null +++ b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + A new method :meth:`.CalibrationEntry.get_duration` has been added to + calibration entries. This method returns duration of calibration + in units of dt when available, otherwise returns None. +upgrade: + - | + :meth:`.Target.update_from_instruction_schedule_map` no longer raises + KeyError nor ValueError when qubits are missing in the target instruction + or inst_name_map is not provided for undefined instruction. + In the former case, it just ignores the inst map definition for undefined qubits. + In the latter case, gate mapping is pulled from the standard Qiskit gates + and finally custom :class:`.Gate` object is defined from the schedule name. + - | + :class:`PulseGates` transpiler pass has been upgraded to respect + ``inst_map`` when ``target`` is also provided, rather than ignoring it. + The pass now uses the target data to find custom calibration, but it + copies instruction schedule map into it before transforming the DAG circuit. +fixes: + - | + A bug that custom gates in the instruction schedule map is ignored + when transpiling a circuit with V2 backend has been fixed. diff --git a/test/python/pulse/test_calibration_entries.py b/test/python/pulse/test_calibration_entries.py index 3259d3b8a6aa..aaf33926e56e 100644 --- a/test/python/pulse/test_calibration_entries.py +++ b/test/python/pulse/test_calibration_entries.py @@ -40,10 +40,12 @@ class TestSchedule(QiskitTestCase): def test_add_schedule(self): """Basic test pulse Schedule format.""" + ref_duration = 10 + program = Schedule() program.insert( 0, - Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), + Play(Constant(duration=ref_duration, amp=0.1, angle=0.0), DriveChannel(0)), inplace=True, ) @@ -58,11 +60,15 @@ def test_add_schedule(self): schedule_ref = program self.assertEqual(schedule_to_test, schedule_ref) + self.assertEqual(entry.get_duration(), ref_duration) + def test_add_block(self): """Basic test pulse Schedule format.""" + ref_duration = 10 + program = ScheduleBlock() program.append( - Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), + Play(Constant(duration=ref_duration, amp=0.1, angle=0.0), DriveChannel(0)), inplace=True, ) @@ -77,6 +83,8 @@ def test_add_block(self): schedule_ref = program self.assertEqual(schedule_to_test, schedule_ref) + self.assertEqual(entry.get_duration(), ref_duration) + def test_parameterized_schedule(self): """Test adding and managing parameterized schedule.""" param1 = Parameter("P1") @@ -99,6 +107,9 @@ def test_parameterized_schedule(self): schedule_ref = program.assign_parameters({param1: 10, param2: 0.1}, inplace=False) self.assertEqual(schedule_to_test, schedule_ref) + # Undetermined + self.assertEqual(entry.get_duration(), None) + def test_parameterized_schedule_with_user_args(self): """Test adding schedule with user signature. @@ -207,6 +218,8 @@ def factory(): schedule_ref = program self.assertEqual(schedule_to_test, schedule_ref) + self.assertEqual(entry.get_duration(), None) + def test_add_callable_with_argument(self): """Basic test callable format.""" @@ -308,6 +321,9 @@ def test_add_qobj(self): entry = PulseQobjDef(converter=self.converter, name="my_gate") entry.define(serialized_program) + self.assertEqual(entry.get_duration(), 25) + self.assertIsNone(entry._definition) # Check if schedule is not parsed + signature_to_test = list(entry.get_signature().parameters.keys()) signature_ref = [] self.assertListEqual(signature_to_test, signature_ref) diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index beb08d5e66fc..2ddec714c7db 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -264,3 +264,88 @@ def test_transpile_with_different_qubit(self): transpiled_qc = transpile(qc, backend, initial_layout=[3]) self.assertDictEqual(transpiled_qc.calibrations, {}) + + def test_transpile_with_both_instmap_and_empty_target(self): + """Test when instmap and target are both provided + and only instmap contains custom schedules. + + Test case from Qiskit/qiskit-terra/#9489 + """ + instmap = FakeAthens().defaults().instruction_schedule_map + instmap.add("sx", (0,), self.custom_sx_q0) + instmap.add("sx", (1,), self.custom_sx_q1) + + # This doesn't have custom schedule definition + target = FakeAthensV2().target + + qc = circuit.QuantumCircuit(2) + qc.sx(0) + qc.x(0) + qc.rz(0, 0) + qc.sx(1) + qc.measure_all() + + transpiled_qc = transpile( + qc, + basis_gates=["sx", "rz", "x"], + inst_map=instmap, + target=target, + initial_layout=[0, 1], + ) + ref_calibration = { + "sx": { + ((0,), ()): self.custom_sx_q0, + ((1,), ()): self.custom_sx_q1, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_instmap_with_v2backend(self): + """Test when instmap is provided with V2 backend. + + Test case from Qiskit/qiskit-terra/#9489 + """ + instmap = FakeAthens().defaults().instruction_schedule_map + instmap.add("sx", (0,), self.custom_sx_q0) + instmap.add("sx", (1,), self.custom_sx_q1) + + qc = circuit.QuantumCircuit(2) + qc.sx(0) + qc.x(0) + qc.rz(0, 0) + qc.sx(1) + qc.measure_all() + + transpiled_qc = transpile(qc, FakeAthensV2(), inst_map=instmap, initial_layout=[0, 1]) + ref_calibration = { + "sx": { + ((0,), ()): self.custom_sx_q0, + ((1,), ()): self.custom_sx_q1, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_instmap_with_v2backend_with_custom_gate(self): + """Test when instmap is provided with V2 backend. + + In this test case, instmap contains a custom gate which doesn't belong to + Qiskit standard gate. Target must define a custom gete on the fly + to reflect user-provided instmap. + + Test case from Qiskit/qiskit-terra/#9489 + """ + instmap = FakeAthens().defaults().instruction_schedule_map + instmap.add("rabi12", (0,), self.custom_sx_q0) + + gate = circuit.Gate("rabi12", 1, []) + qc = circuit.QuantumCircuit(1) + qc.append(gate, [0]) + qc.measure_all() + + transpiled_qc = transpile(qc, FakeAthensV2(), inst_map=instmap, initial_layout=[0]) + ref_calibration = { + "rabi12": { + ((0,), ()): self.custom_sx_q0, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index e3980eff1814..0b3f8c55e685 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1177,16 +1177,17 @@ def test_update_from_instruction_schedule_map_new_instruction_no_name_map(self): inst_map = InstructionScheduleMap() inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, self.custom_sx_q1) - with self.assertRaises(ValueError): - target.update_from_instruction_schedule_map(inst_map) + target.update_from_instruction_schedule_map(inst_map) + self.assertEqual(target["sx"][(0,)].calibration, self.custom_sx_q0) + self.assertEqual(target["sx"][(1,)].calibration, self.custom_sx_q1) def test_update_from_instruction_schedule_map_new_qarg_raises(self): inst_map = InstructionScheduleMap() inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, self.custom_sx_q1) inst_map.add("sx", 2, self.custom_sx_q1) - with self.assertRaises(KeyError): - self.pulse_target.update_from_instruction_schedule_map(inst_map) + self.pulse_target.update_from_instruction_schedule_map(inst_map) + self.assertFalse(self.pulse_target.instruction_supported("sx", (2,))) def test_update_from_instruction_schedule_map_with_dt_set(self): inst_map = InstructionScheduleMap() From c7f10c04ae24bae0ee7c68a2d547e627040c7397 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 15 Feb 2023 23:27:34 +0900 Subject: [PATCH 02/14] Remove fix note --- ...date-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml index 8281f85ad6b1..513e30f6582e 100644 --- a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml +++ b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml @@ -12,12 +12,3 @@ upgrade: In the former case, it just ignores the inst map definition for undefined qubits. In the latter case, gate mapping is pulled from the standard Qiskit gates and finally custom :class:`.Gate` object is defined from the schedule name. - - | - :class:`PulseGates` transpiler pass has been upgraded to respect - ``inst_map`` when ``target`` is also provided, rather than ignoring it. - The pass now uses the target data to find custom calibration, but it - copies instruction schedule map into it before transforming the DAG circuit. -fixes: - - | - A bug that custom gates in the instruction schedule map is ignored - when transpiling a circuit with V2 backend has been fixed. From bfd33d4199545f05cba3c9f835e02ed7f3c3466e Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 16 Feb 2023 13:40:22 +0900 Subject: [PATCH 03/14] Remove get_duration --- qiskit/pulse/calibration_entries.py | 44 +------------------ qiskit/transpiler/target.py | 31 ++++++++----- test/python/pulse/test_calibration_entries.py | 20 +-------- 3 files changed, 22 insertions(+), 73 deletions(-) diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py index 80babaf88260..29f32b89f04d 100644 --- a/qiskit/pulse/calibration_entries.py +++ b/qiskit/pulse/calibration_entries.py @@ -16,7 +16,7 @@ from enum import IntEnum from typing import Callable, List, Union, Optional, Sequence, Any -from qiskit.pulse.exceptions import PulseError, UnassignedDurationError +from qiskit.pulse.exceptions import PulseError from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.qobj.converters import QobjToInstructionConverter from qiskit.qobj.pulse_qobj import PulseQobjInstruction @@ -64,15 +64,6 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: """ pass - @abstractmethod - def get_duration(self) -> int: - """Get duration of schedule. - - Returns: - Integer value representing a duration of schedule in units of dt. - """ - pass - class ScheduleDef(CalibrationEntry): """In-memory Qiskit Pulse representation. @@ -155,12 +146,6 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: pass return self._definition.assign_parameters(value_dict, inplace=False) - def get_duration(self) -> int: - try: - return self._definition.duration - except UnassignedDurationError: - return None - def __eq__(self, other): # This delegates equality check to Schedule or ScheduleBlock. return self._definition == other._definition @@ -208,10 +193,6 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: schedule.metadata["publisher"] = CalibrationPublisher.QISKIT return schedule - def get_duration(self) -> int: - # Duration is undetermined until funciton is called with full arguments. - return None - def __eq__(self, other): # We cannot evaluate function equality without parsing python AST. # This simply compares wether they are the same object. @@ -275,29 +256,6 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: self._build_schedule() return super().get_schedule(*args, **kwargs) - def get_duration(self) -> int: - if self._definition: - return self._definition.duration - - # Parse Qobj instruction and find max instruction end time - t_max = 0 - for inst in self._source: - if inst.name == "parametric_pulse": - # Play instruction - duration = inst.parameters.get("duration", 0) - elif inst.name not in self._converter.get_supported_instructions(): - # Play instruction with Waveform - try: - # Get samples from the pulse library dictionary - duration = len(self._converter._pulse_library[inst.name]) - except KeyError: - continue - else: - # Acquire, Delay instruction - duration = getattr(inst, "duration", 0) - t_max = max(t_max, inst.t0 + duration) - return t_max - def __eq__(self, other): if isinstance(other, PulseQobjDef): # If both objects are Qobj just check Qobj equality. diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 3cc7e6c6d8ba..fb15e71a301f 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -443,27 +443,34 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err """ for inst_name in inst_map.instructions: out_props = {} - for qarg in inst_map.qubits_with_instruction(inst_name): + for qargs in inst_map.qubits_with_instruction(inst_name): try: - qarg = tuple(qarg) + qargs = tuple(qargs) except TypeError: - qarg = (qarg,) - entry = inst_map._get_calibration_entry(inst_name, qarg) - val = InstructionProperties(calibration=entry) + qargs = (qargs,) + entry = inst_map._get_calibration_entry(inst_name, qargs) + if inst_name in self._gate_map and qargs in self._gate_map[inst_name]: + default_entry = self._gate_map[inst_name][qargs]._calibration + if entry == default_entry: + # Skip parsing existing entry, e.g. backend calibrated schedule. + continue if self.dt is not None: - val.duration = entry.get_duration() * self.dt + duration = entry.get_schedule().duration * self.dt else: - val.duration = None + duration = None if inst_name in self._gate_map and error_dict is not None: error_inst = error_dict.get(inst_name) if error_inst: - error = error_inst.get(qarg) - val.error = error + error = error_inst.get(qargs) else: - val.error = None + error = None else: - val.error = None - out_props[qarg] = val + error = None + out_props[qargs] = InstructionProperties( + duration=duration, + error=error, + calibration=entry, + ) if inst_name not in self._gate_map: if inst_name_map is None: inst_name_map = get_standard_gate_name_mapping() diff --git a/test/python/pulse/test_calibration_entries.py b/test/python/pulse/test_calibration_entries.py index aaf33926e56e..3259d3b8a6aa 100644 --- a/test/python/pulse/test_calibration_entries.py +++ b/test/python/pulse/test_calibration_entries.py @@ -40,12 +40,10 @@ class TestSchedule(QiskitTestCase): def test_add_schedule(self): """Basic test pulse Schedule format.""" - ref_duration = 10 - program = Schedule() program.insert( 0, - Play(Constant(duration=ref_duration, amp=0.1, angle=0.0), DriveChannel(0)), + Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), inplace=True, ) @@ -60,15 +58,11 @@ def test_add_schedule(self): schedule_ref = program self.assertEqual(schedule_to_test, schedule_ref) - self.assertEqual(entry.get_duration(), ref_duration) - def test_add_block(self): """Basic test pulse Schedule format.""" - ref_duration = 10 - program = ScheduleBlock() program.append( - Play(Constant(duration=ref_duration, amp=0.1, angle=0.0), DriveChannel(0)), + Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), inplace=True, ) @@ -83,8 +77,6 @@ def test_add_block(self): schedule_ref = program self.assertEqual(schedule_to_test, schedule_ref) - self.assertEqual(entry.get_duration(), ref_duration) - def test_parameterized_schedule(self): """Test adding and managing parameterized schedule.""" param1 = Parameter("P1") @@ -107,9 +99,6 @@ def test_parameterized_schedule(self): schedule_ref = program.assign_parameters({param1: 10, param2: 0.1}, inplace=False) self.assertEqual(schedule_to_test, schedule_ref) - # Undetermined - self.assertEqual(entry.get_duration(), None) - def test_parameterized_schedule_with_user_args(self): """Test adding schedule with user signature. @@ -218,8 +207,6 @@ def factory(): schedule_ref = program self.assertEqual(schedule_to_test, schedule_ref) - self.assertEqual(entry.get_duration(), None) - def test_add_callable_with_argument(self): """Basic test callable format.""" @@ -321,9 +308,6 @@ def test_add_qobj(self): entry = PulseQobjDef(converter=self.converter, name="my_gate") entry.define(serialized_program) - self.assertEqual(entry.get_duration(), 25) - self.assertIsNone(entry._definition) # Check if schedule is not parsed - signature_to_test = list(entry.get_signature().parameters.keys()) signature_ref = [] self.assertListEqual(signature_to_test, signature_ref) From a2567439a3f2809718f3f67d64b8e35040429468 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 16 Feb 2023 13:42:07 +0900 Subject: [PATCH 04/14] Update the logic to get instruction object --- qiskit/transpiler/target.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index fb15e71a301f..a3b51e12dd20 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -475,25 +475,33 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err if inst_name_map is None: inst_name_map = get_standard_gate_name_mapping() if inst_name in inst_name_map: + # Remove qargs with length that doesn't match with instruction qubit number inst_obj = inst_name_map[inst_name] + normalized_props = {} + for qargs, prop in out_props.items(): + if len(qargs) != inst_obj.num_qubits: + continue + normalized_props[qargs] = prop + self.add_instruction(inst_obj, normalized_props, name=inst_name) else: - # Custom gate object, which doesn't belong to standard Qiskit gates. + # Assumes qubit number and parameter names are consistent + qargs0 = inst_map.qubits_with_instruction(inst_name)[0] + try: + qargs0 = tuple(qargs0) + except TypeError: + qargs0 = (qargs0,) + cal = getattr(out_props[qargs0], "_calibration") inst_obj = Gate( name=inst_name, - num_qubits=len(qarg), - params=list(map(Parameter, entry.get_signature().parameters.keys())), + num_qubits=len(qargs0), + params=list(map(Parameter, cal.get_signature().parameters.keys())), ) - normalized_props = {} - for qargs, prop in out_props.items(): - if len(qargs) != inst_obj.num_qubits: - continue - normalized_props[qargs] = prop - self.add_instruction(inst_obj, normalized_props, name=inst_name) + self.add_instruction(inst_obj, out_props, name=inst_name) else: - for qarg, prop in out_props.items(): - if qarg not in self._gate_map[inst_name]: + for qargs, prop in out_props.items(): + if qargs not in self._gate_map[inst_name]: continue - self.update_instruction_properties(inst_name, qarg, prop) + self.update_instruction_properties(inst_name, qargs, prop) @property def qargs(self): From dd4ad0c348842b67bacc36d4f7db1c67026a5887 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 16 Feb 2023 13:42:25 +0900 Subject: [PATCH 05/14] Update target immediately when inst map is available --- qiskit/compiler/transpiler.py | 2 ++ qiskit/transpiler/passes/calibration/pulse_gate.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index eca13cde7bd1..4224e41d53a3 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -659,6 +659,8 @@ def _parse_transpile_args( durations = _parse_instruction_durations(backend, instruction_durations, dt, circuits) timing_constraints = _parse_timing_constraints(backend, timing_constraints, num_circuits) target = _parse_target(backend, target) + if inst_map is not None and inst_map.has_custom_gate() and target is not None: + target.update_from_instruction_schedule_map(inst_map) if scheduling_method and any(d is None for d in durations): raise TranspilerError( "Transpiling a circuit with a scheduling method" diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py index f6f2f675b5f3..ca81316754e9 100644 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ b/qiskit/transpiler/passes/calibration/pulse_gate.py @@ -62,9 +62,11 @@ def __init__( """ super().__init__() + if inst_map is None and target is None: + raise TranspilerError("inst_map and target cannot be None simulataneously.") + if target is None: target = Target() - if inst_map is not None: target.update_from_instruction_schedule_map(inst_map) self.target = target From 5207b4716a054c880bb2cd50227813899a82b3ab Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 16 Feb 2023 15:36:36 +0900 Subject: [PATCH 06/14] Update tests --- .../python/transpiler/test_pulse_gate_pass.py | 64 +++++++++++++------ test/python/transpiler/test_target.py | 14 ++-- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index 2ddec714c7db..70bab9f11460 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -12,39 +12,43 @@ """Transpiler pulse gate pass testing.""" +import ddt + from qiskit import pulse, circuit, transpile from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeAthens, FakeAthensV2 +from qiskit.quantum_info.random import random_unitary +@ddt.ddt class TestPulseGate(QiskitTestCase): """Integration test of pulse gate pass with custom backend.""" def setUp(self): super().setUp() + self.sched_param = circuit.Parameter("P0") + with pulse.build(name="sx_q0") as custom_sx_q0: pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) - self.custom_sx_q0 = custom_sx_q0 with pulse.build(name="sx_q1") as custom_sx_q1: pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) - self.custom_sx_q1 = custom_sx_q1 - self.sched_param = circuit.Parameter("P0") + with pulse.build(name="cx_q01") as custom_cx_q01: + pulse.play(pulse.Constant(100, 0.4), pulse.ControlChannel(0)) + self.custom_cx_q01 = custom_cx_q01 with pulse.build(name="my_gate_q0") as my_gate_q0: pulse.shift_phase(self.sched_param, pulse.DriveChannel(0)) pulse.play(pulse.Constant(120, 0.1), pulse.DriveChannel(0)) - self.my_gate_q0 = my_gate_q0 with pulse.build(name="my_gate_q1") as my_gate_q1: pulse.shift_phase(self.sched_param, pulse.DriveChannel(1)) pulse.play(pulse.Constant(120, 0.2), pulse.DriveChannel(1)) - self.my_gate_q1 = my_gate_q1 def test_transpile_with_bare_backend(self): @@ -265,7 +269,8 @@ def test_transpile_with_different_qubit(self): self.assertDictEqual(transpiled_qc.calibrations, {}) - def test_transpile_with_both_instmap_and_empty_target(self): + @ddt.data(0, 1, 2, 3) + def test_transpile_with_both_instmap_and_empty_target(self, opt_level): """Test when instmap and target are both provided and only instmap contains custom schedules. @@ -274,20 +279,19 @@ def test_transpile_with_both_instmap_and_empty_target(self): instmap = FakeAthens().defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) + instmap.add("cx", (0, 1), self.custom_cx_q01) # This doesn't have custom schedule definition target = FakeAthensV2().target qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) + qc.append(random_unitary(4, seed=123), [0, 1]) qc.measure_all() transpiled_qc = transpile( qc, - basis_gates=["sx", "rz", "x"], + optimization_level=opt_level, + basis_gates=["sx", "rz", "x", "cx"], inst_map=instmap, target=target, initial_layout=[0, 1], @@ -296,11 +300,15 @@ def test_transpile_with_both_instmap_and_empty_target(self): "sx": { ((0,), ()): self.custom_sx_q0, ((1,), ()): self.custom_sx_q1, - } + }, + "cx": { + ((0, 1), ()): self.custom_cx_q01, + }, } self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - def test_transpile_with_instmap_with_v2backend(self): + @ddt.data(0, 1, 2, 3) + def test_transpile_with_instmap_with_v2backend(self, opt_level): """Test when instmap is provided with V2 backend. Test case from Qiskit/qiskit-terra/#9489 @@ -308,24 +316,32 @@ def test_transpile_with_instmap_with_v2backend(self): instmap = FakeAthens().defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) + instmap.add("cx", (0, 1), self.custom_cx_q01) qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) + qc.append(random_unitary(4, seed=123), [0, 1]) qc.measure_all() - transpiled_qc = transpile(qc, FakeAthensV2(), inst_map=instmap, initial_layout=[0, 1]) + transpiled_qc = transpile( + qc, + FakeAthensV2(), + optimization_level=opt_level, + inst_map=instmap, + initial_layout=[0, 1], + ) ref_calibration = { "sx": { ((0,), ()): self.custom_sx_q0, ((1,), ()): self.custom_sx_q1, - } + }, + "cx": { + ((0, 1), ()): self.custom_cx_q01, + }, } self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - def test_transpile_with_instmap_with_v2backend_with_custom_gate(self): + @ddt.data(0, 1, 2, 3) + def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level): """Test when instmap is provided with V2 backend. In this test case, instmap contains a custom gate which doesn't belong to @@ -342,7 +358,13 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self): qc.append(gate, [0]) qc.measure_all() - transpiled_qc = transpile(qc, FakeAthensV2(), inst_map=instmap, initial_layout=[0]) + transpiled_qc = transpile( + qc, + FakeAthensV2(), + optimization_level=opt_level, + inst_map=instmap, + initial_layout=[0], + ) ref_calibration = { "rabi12": { ((0,), ()): self.custom_sx_q0, diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 0b3f8c55e685..942f37a0dfe7 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1167,8 +1167,10 @@ def test_update_from_instruction_schedule_map_update_schedule(self): inst_map.add("sx", 1, custom_sx) self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) - self.assertIsNone(self.pulse_target["sx"][(0,)].duration) - self.assertIsNone(self.pulse_target["sx"][(0,)].error) + # Calibration doesn't change for q0 + self.assertEqual(self.pulse_target["sx"][(0,)].duration, 35.5e-9) + self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) + # Calibration is updated for q1 without error dict and gate time self.assertIsNone(self.pulse_target["sx"][(1,)].duration) self.assertIsNone(self.pulse_target["sx"][(1,)].error) @@ -1201,7 +1203,11 @@ def test_update_from_instruction_schedule_map_with_dt_set(self): self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) self.assertEqual(self.pulse_target["sx"][(1,)].duration, 1000.0) self.assertIsNone(self.pulse_target["sx"][(1,)].error) - self.assertIsNone(self.pulse_target["sx"][(0,)].error) + # This is an edge case. + # System dt is read-only property and changing it will break all underlying calibrations. + # duration of sx0 returns previous value since calibration doesn't change. + self.assertEqual(self.pulse_target["sx"][(0,)].duration, 35.5e-9) + self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) def test_update_from_instruction_schedule_map_with_error_dict(self): inst_map = InstructionScheduleMap() @@ -1217,7 +1223,7 @@ def test_update_from_instruction_schedule_map_with_error_dict(self): inst_map, {"sx": SXGate()}, error_dict=error_dict ) self.assertEqual(self.pulse_target["sx"][(1,)].error, 1.0) - self.assertIsNone(self.pulse_target["sx"][(0,)].error) + self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) def test_timing_constraints(self): generated_constraints = self.pulse_target.timing_constraints() From 939ad61a3a9a03f3ae57b35bcbfaff4c08a7e75c Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Fri, 10 Mar 2023 23:04:30 +0900 Subject: [PATCH 07/14] Fix edge case. IBM backend still provide ugate calibrations in CmdDef and they are loaded in the instmap. If we update target with the instmap, these gates are accidentally registered in the target, and they may be used in the following 1q decomposition. To prevent this, update_from_instruction_schedule_map method is updated. --- qiskit/providers/models/pulsedefaults.py | 2 +- qiskit/pulse/calibration_entries.py | 107 ++++++++++++------ qiskit/pulse/instruction_schedule_map.py | 6 +- .../passes/calibration/pulse_gate.py | 18 +-- qiskit/transpiler/target.py | 105 +++++++++++++---- .../python/transpiler/test_pulse_gate_pass.py | 9 +- test/python/transpiler/test_target.py | 29 ++++- 7 files changed, 197 insertions(+), 79 deletions(-) diff --git a/qiskit/providers/models/pulsedefaults.py b/qiskit/providers/models/pulsedefaults.py index c65aeb256396..2ee1594571e1 100644 --- a/qiskit/providers/models/pulsedefaults.py +++ b/qiskit/providers/models/pulsedefaults.py @@ -205,7 +205,7 @@ def __init__( for inst in cmd_def: entry = PulseQobjDef(converter=self.converter, name=inst.name) - entry.define(inst.sequence) + entry.define(inst.sequence, user_provided=False) self.instruction_schedule_map._add( instruction_name=inst.name, qubits=tuple(inst.qubits), diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py index 29f32b89f04d..1aa64b8779b7 100644 --- a/qiskit/pulse/calibration_entries.py +++ b/qiskit/pulse/calibration_entries.py @@ -34,11 +34,12 @@ class CalibrationEntry(metaclass=ABCMeta): """A metaclass of a calibration entry.""" @abstractmethod - def define(self, definition: Any): + def define(self, definition: Any, user_provided: bool): """Attach definition to the calibration entry. Args: definition: Definition of this entry. + user_provided: If this entry is defined by user. """ pass @@ -64,6 +65,12 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: """ pass + @property + @abstractmethod + def user_provided(self) -> bool: + """Return if this entry is user defined.""" + pass + class ScheduleDef(CalibrationEntry): """In-memory Qiskit Pulse representation. @@ -90,6 +97,11 @@ def __init__(self, arguments: Optional[Sequence[str]] = None): self._definition = None self._signature = None + self._user_provided = None + + @property + def user_provided(self) -> bool: + return self._user_provided def _parse_argument(self): """Generate signature from program and user provided argument names.""" @@ -120,35 +132,48 @@ def _parse_argument(self): ) self._signature = signature - def define(self, definition: Union[Schedule, ScheduleBlock]): + def define( + self, + definition: Union[Schedule, ScheduleBlock], + user_provided: bool = True, + ): self._definition = definition - # add metadata - if "publisher" not in definition.metadata: - definition.metadata["publisher"] = CalibrationPublisher.QISKIT self._parse_argument() + self._user_provided = user_provided def get_signature(self) -> inspect.Signature: return self._signature def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: if not args and not kwargs: - return self._definition - try: - to_bind = self.get_signature().bind_partial(*args, **kwargs) - except TypeError as ex: - raise PulseError("Assigned parameter doesn't match with schedule parameters.") from ex - value_dict = {} - for param in self._definition.parameters: - # Schedule allows partial bind. This results in parameterized Schedule. + out = self._definition + else: try: - value_dict[param] = to_bind.arguments[param.name] - except KeyError: - pass - return self._definition.assign_parameters(value_dict, inplace=False) + to_bind = self.get_signature().bind_partial(*args, **kwargs) + except TypeError as ex: + raise PulseError( + "Assigned parameter doesn't match with schedule parameters." + ) from ex + value_dict = {} + for param in self._definition.parameters: + # Schedule allows partial bind. This results in parameterized Schedule. + try: + value_dict[param] = to_bind.arguments[param.name] + except KeyError: + pass + out = self._definition.assign_parameters(value_dict, inplace=False) + if "publisher" not in out.metadata: + if self.user_provided: + out.metadata["publisher"] = CalibrationPublisher.QISKIT + else: + out.metadata["publisher"] = CalibrationPublisher.BACKEND_PROVIDER + return out def __eq__(self, other): # This delegates equality check to Schedule or ScheduleBlock. - return self._definition == other._definition + if hasattr(other, "_definition"): + return self._definition == other._definition + return False def __str__(self): out = f"Schedule {self._definition.name}" @@ -171,10 +196,20 @@ def __init__(self): """Define an empty entry.""" self._definition = None self._signature = None + self._user_provided = None + + @property + def user_provided(self) -> bool: + return self._user_provided - def define(self, definition: Callable): + def define( + self, + definition: Callable, + user_provided: bool = True, + ): self._definition = definition self._signature = inspect.signature(definition) + self._user_provided = user_provided def get_signature(self) -> inspect.Signature: return self._signature @@ -186,17 +221,20 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: to_bind.apply_defaults() except TypeError as ex: raise PulseError("Assigned parameter doesn't match with function signature.") from ex - - schedule = self._definition(**to_bind.arguments) - # add metadata - if "publisher" not in schedule.metadata: - schedule.metadata["publisher"] = CalibrationPublisher.QISKIT - return schedule + out = self._definition(**to_bind.arguments) + if "publisher" not in out.metadata: + if self.user_provided: + out.metadata["publisher"] = CalibrationPublisher.QISKIT + else: + out.metadata["publisher"] = CalibrationPublisher.BACKEND_PROVIDER + return out def __eq__(self, other): # We cannot evaluate function equality without parsing python AST. - # This simply compares wether they are the same object. - return self._definition is other._definition + # This simply compares weather they are the same object. + if hasattr(other, "_definition"): + return self._definition == other._definition + return False def __str__(self): params_str = ", ".join(self.get_signature().parameters.keys()) @@ -237,14 +275,17 @@ def _build_schedule(self): for qobj_inst in self._source: for qiskit_inst in self._converter._get_sequences(qobj_inst): schedule.insert(qobj_inst.t0, qiskit_inst, inplace=True) - schedule.metadata["publisher"] = CalibrationPublisher.BACKEND_PROVIDER - self._definition = schedule self._parse_argument() - def define(self, definition: List[PulseQobjInstruction]): + def define( + self, + definition: List[PulseQobjInstruction], + user_provided: bool = False, + ): # This doesn't generate signature immediately, because of lazy schedule build. self._source = definition + self._user_provided = user_provided def get_signature(self) -> inspect.Signature: if self._definition is None: @@ -261,9 +302,11 @@ def __eq__(self, other): # If both objects are Qobj just check Qobj equality. return self._source == other._source if isinstance(other, ScheduleDef) and self._definition is None: - # To compare with other scheudle def, this also generates schedule object from qobj. + # To compare with other schedule def, this also generates schedule object from qobj. self._build_schedule() - return self._definition == other._definition + if hasattr(other, "_definition"): + return self._definition == other._definition + return False def __str__(self): if self._definition is None: diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index c5d144d11ecd..7fe22cef1e94 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -39,8 +39,8 @@ CalibrationEntry, ScheduleDef, CallableDef, - PulseQobjDef, # for backward compatibility + PulseQobjDef, CalibrationPublisher, ) from qiskit.pulse.exceptions import PulseError @@ -77,7 +77,7 @@ def has_custom_gate(self) -> bool: """Return ``True`` if the map has user provided instruction.""" for qubit_inst in self._map.values(): for entry in qubit_inst.values(): - if not isinstance(entry, PulseQobjDef): + if entry.user_provided: return True return False @@ -264,7 +264,7 @@ def add( "Supplied schedule must be one of the Schedule, ScheduleBlock or a " "callable that outputs a schedule." ) - entry.define(schedule) + entry.define(schedule, user_provided=True) self._add(instruction, qubits, entry) def _add( diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py index ca81316754e9..71536ed89fd5 100644 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ b/qiskit/transpiler/passes/calibration/pulse_gate.py @@ -80,7 +80,7 @@ def supported(self, node_op: CircuitInst, qubits: List) -> bool: Returns: Return ``True`` is calibration can be provided. """ - return self.target.instruction_supported(operation_name=node_op.name, qargs=qubits) + return self.target.calibration_supported(node_op.name, tuple(qubits)) def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: """Gets the calibrated schedule for the given instruction and qubits. @@ -95,18 +95,4 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, Raises: TranspilerError: When node is parameterized and calibration is raw schedule object. """ - inst_property = self.target[node_op.name][tuple(qubits)] - if not node_op.params: - return inst_property.calibration - try: - # CircuitInstruction doesn't preserve parameter name after parameter binding. - # Thus schedule cannot generate bind dictionary. - # Use CalibraionEntry to utilize inspected signature object. - calibration_entry = inst_property._calibration - return calibration_entry.get_schedule(*node_op.params) - except AttributeError as ex: - raise TranspilerError( - f"Calibraton for {node_op.name} of {qubits} is not a CalibraryEntry instance. " - f"Mapping from parameter values {node_op.params} to parameter objects " - f"in the schedule cannot be identified." - ) from ex + return self.target.get_calibration(node_op.name, tuple(qubits), *node_op.params) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index a3b51e12dd20..be4ae6e51c15 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -17,7 +17,7 @@ from a backend """ import warnings -from typing import Union +from typing import Tuple, Union from collections.abc import Mapping from collections import defaultdict import datetime @@ -28,6 +28,7 @@ import rustworkx as rx from qiskit.circuit.parameter import Parameter +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.gate import Gate from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap @@ -91,7 +92,7 @@ def calibration(self): def calibration(self, calibration: Union[Schedule, ScheduleBlock, CalibrationEntry]): if isinstance(calibration, (Schedule, ScheduleBlock)): new_entry = ScheduleDef() - new_entry.define(calibration) + new_entry.define(calibration, user_provided=True) else: new_entry = calibration self._calibration = new_entry @@ -441,6 +442,8 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err this dictionary. If one is not found in ``error_dict`` then ``None`` will be used. """ + get_calibration = getattr(inst_map, "_get_calibration_entry") + for inst_name in inst_map.instructions: out_props = {} for qargs in inst_map.qubits_with_instruction(inst_name): @@ -448,29 +451,36 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err qargs = tuple(qargs) except TypeError: qargs = (qargs,) - entry = inst_map._get_calibration_entry(inst_name, qargs) - if inst_name in self._gate_map and qargs in self._gate_map[inst_name]: - default_entry = self._gate_map[inst_name][qargs]._calibration - if entry == default_entry: - # Skip parsing existing entry, e.g. backend calibrated schedule. - continue - if self.dt is not None: - duration = entry.get_schedule().duration * self.dt - else: - duration = None - if inst_name in self._gate_map and error_dict is not None: - error_inst = error_dict.get(inst_name) - if error_inst: - error = error_inst.get(qargs) + try: + props = self._gate_map[inst_name][qargs] + except (KeyError, TypeError): + props = None + + entry = get_calibration(inst_name, qargs) + if entry.user_provided and getattr(props, "_calibration", None) != entry: + # It only copies user-provided calibration from the inst map. + # Backend defined entry must already exist in Target. + if self.dt is not None: + duration = entry.get_schedule().duration * self.dt else: - error = None + duration = None + props = InstructionProperties( + duration=duration, + calibration=entry, + ) else: - error = None - out_props[qargs] = InstructionProperties( - duration=duration, - error=error, - calibration=entry, - ) + if props is None: + # Edge case. Calibration is backend defined, but this is not + # registered in the backend target. Ignore this entry. + continue + try: + # Update gate error if provided. + props.error = error_dict[inst_name][qargs] + except (KeyError, TypeError): + pass + out_props[qargs] = props + if not out_props: + continue if inst_name not in self._gate_map: if inst_name_map is None: inst_name_map = get_standard_gate_name_mapping() @@ -797,6 +807,55 @@ def check_obj_params(parameters, obj): ) return False + def calibration_supported( + self, + operation_name: str, + qargs: Tuple[int, ...], + ) -> bool: + """Return whether the instruction (operation + qubits) defines a calibration. + + Args: + operation_name: The name of the operation for the instruction. + qargs: The tuple of qubit indices for the instruction. + + Returns: + Returns ``True`` if the calibration is supported and ``False`` if it isn't. + """ + qargs = tuple(qargs) + if operation_name not in self._gate_map: + return False + if qargs not in self._gate_map[operation_name]: + return False + return getattr(self._gate_map[operation_name][qargs], "_calibration") is not None + + def get_calibration( + self, + operation_name: str, + qargs: Tuple[int, ...], + *args: ParameterValueType, + **kwargs: ParameterValueType, + ) -> Union[Schedule, ScheduleBlock]: + """Get calibrated pulse schedule for the instruction. + + If calibration is templated with parameters, one can also provide those values + to build a schedule with assigned parameters. + + Args: + operation_name: The name of the operation for the instruction. + qargs: The tuple of qubit indices for the instruction. + args: Parameter values to build schedule if any. + kwargs: Parameter values with name to build schedule if any. + + Returns: + Calibrated pulse schedule of corresponding instruction. + """ + if not self.calibration_supported(operation_name, qargs): + raise KeyError( + f"Calibration of instruction {operation_name} for qubit {qargs} is not defined." + ) + cal_entry = getattr(self._gate_map[operation_name][qargs], "_calibration") + return cal_entry.get_schedule(*args, **kwargs) + @property def operation_names(self): """Get the operation names in the target.""" diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index 70bab9f11460..0ada1e03a04e 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -15,9 +15,9 @@ import ddt from qiskit import pulse, circuit, transpile -from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeAthens, FakeAthensV2 from qiskit.quantum_info.random import random_unitary +from qiskit.test import QiskitTestCase @ddt.ddt @@ -350,8 +350,11 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level) Test case from Qiskit/qiskit-terra/#9489 """ + with pulse.build(name="custom") as rabi12: + pulse.play(pulse.Constant(100, 0.4), pulse.DriveChannel(0)) + instmap = FakeAthens().defaults().instruction_schedule_map - instmap.add("rabi12", (0,), self.custom_sx_q0) + instmap.add("rabi12", (0,), rabi12) gate = circuit.Gate("rabi12", 1, []) qc = circuit.QuantumCircuit(1) @@ -367,7 +370,7 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level) ) ref_calibration = { "rabi12": { - ((0,), ()): self.custom_sx_q0, + ((0,), ()): rabi12, } } self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 942f37a0dfe7..980daef3d9bd 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -35,7 +35,7 @@ from qiskit.circuit.parameter import Parameter from qiskit import pulse from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.pulse.calibration_entries import CalibrationPublisher +from qiskit.pulse.calibration_entries import CalibrationPublisher, ScheduleDef from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.timing_constraints import TimingConstraints @@ -1273,6 +1273,33 @@ def test_get_empty_target_calibration(self): self.assertIsNone(target["x"][(0,)].calibration) + def test_loading_legacy_ugate_instmap(self): + # This is typical IBM backend situation. + # IBM provider used to have u1, u2, u3 in the basis gates and + # these have been replaced with sx and rz. + # However, IBM provider still provides calibration of these u gates, + # and the inst map loads them as backend calibrations. + # Target is implicitly updated with inst map when it is set in transpile. + # If u gates are not excluded, they may appear in the transpiled circuit. + # These gates are no longer supported by hardware. + entry = ScheduleDef() + entry.define(pulse.Schedule(name="fake_u3"), user_provided=False) # backend provided + instmap = InstructionScheduleMap() + instmap._add("u3", (0,), entry) + + # Today's standard IBM backend target with sx, rz basis + target = Target() + target.add_instruction(SXGate(), {(0,): InstructionProperties()}) + target.add_instruction(RZGate(Parameter("θ")), {(0,): InstructionProperties()}) + target.add_instruction(Measure(), {(0,): InstructionProperties()}) + names_before = set(target.operation_names) + + target.update_from_instruction_schedule_map(instmap) + names_after = set(target.operation_names) + + # Otherwise u3 and sx-rz basis conflict in 1q decomposition. + self.assertSetEqual(names_before, names_after) + class TestGlobalVariableWidthOperations(QiskitTestCase): def setUp(self): From d10fcf928bc7445d7dc9301cf4332025ba0793d6 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Fri, 10 Mar 2023 23:15:24 +0900 Subject: [PATCH 08/14] cleanup release note --- ...e-gate-pass-for-target-ebfb0ec9571f058e.yaml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml index 513e30f6582e..6487eb1862d6 100644 --- a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml +++ b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml @@ -1,9 +1,16 @@ --- features: - | - A new method :meth:`.CalibrationEntry.get_duration` has been added to - calibration entries. This method returns duration of calibration - in units of dt when available, otherwise returns None. + A new method :meth:`.CalibrationEntry.user_provided` has been added to + calibration entries. This method can be called to check whether the entry + is defined by an end user or backend. + - | + New method :meth:`.Target.calibration_supported` and + :meth:`.Target.get_calibration` have been added to provide + convenient access to the calibration of instruction. + The getter method can be called with parameter args and kwargs, and + it returns a pulse schedule built with parameters when the calibration + is templated with parameters. upgrade: - | :meth:`.Target.update_from_instruction_schedule_map` no longer raises @@ -12,3 +19,7 @@ upgrade: In the former case, it just ignores the inst map definition for undefined qubits. In the latter case, gate mapping is pulled from the standard Qiskit gates and finally custom :class:`.Gate` object is defined from the schedule name. +fixes: + - | + A bug that a custom calibration in :class:`.InstructionScheduleMap` is ignored + when it is set to transpile with :class:`.BackendV2` instance. From bdb790af888455c2f31d5fa7d111a9d697798796 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 15 Mar 2023 01:20:17 +0900 Subject: [PATCH 09/14] Minor review suggestions --- .../transpiler/passes/calibration/pulse_gate.py | 2 +- qiskit/transpiler/target.py | 15 +++++++++------ ...lse-gate-pass-for-target-ebfb0ec9571f058e.yaml | 4 ---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py index 71536ed89fd5..9bfd3c544779 100644 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ b/qiskit/transpiler/passes/calibration/pulse_gate.py @@ -80,7 +80,7 @@ def supported(self, node_op: CircuitInst, qubits: List) -> bool: Returns: Return ``True`` is calibration can be provided. """ - return self.target.calibration_supported(node_op.name, tuple(qubits)) + return self.target.has_calibration(node_op.name, tuple(qubits)) def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: """Gets the calibrated schedule for the given instruction and qubits. diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index be4ae6e51c15..6b60a338bcd8 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -444,6 +444,11 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err """ get_calibration = getattr(inst_map, "_get_calibration_entry") + # Expand name mapping with custom gate name provided by user. + qiskit_inst_name_map = get_standard_gate_name_mapping() + if inst_name_map is not None: + qiskit_inst_name_map.update(inst_name_map) + for inst_name in inst_map.instructions: out_props = {} for qargs in inst_map.qubits_with_instruction(inst_name): @@ -482,11 +487,9 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err if not out_props: continue if inst_name not in self._gate_map: - if inst_name_map is None: - inst_name_map = get_standard_gate_name_mapping() - if inst_name in inst_name_map: + if inst_name in qiskit_inst_name_map: # Remove qargs with length that doesn't match with instruction qubit number - inst_obj = inst_name_map[inst_name] + inst_obj = qiskit_inst_name_map[inst_name] normalized_props = {} for qargs, prop in out_props.items(): if len(qargs) != inst_obj.num_qubits: @@ -807,7 +810,7 @@ def check_obj_params(parameters, obj): ) return False - def calibration_supported( + def has_calibration( self, operation_name: str, qargs: Tuple[int, ...], @@ -849,7 +852,7 @@ def get_calibration( Returns: Calibrated pulse schedule of corresponding instruction. """ - if not self.calibration_supported(operation_name, qargs): + if not self.has_calibration(operation_name, qargs): raise KeyError( f"Calibration of instruction {operation_name} for qubit {qargs} is not defined." ) diff --git a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml index 6487eb1862d6..0504772659cb 100644 --- a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml +++ b/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml @@ -19,7 +19,3 @@ upgrade: In the former case, it just ignores the inst map definition for undefined qubits. In the latter case, gate mapping is pulled from the standard Qiskit gates and finally custom :class:`.Gate` object is defined from the schedule name. -fixes: - - | - A bug that a custom calibration in :class:`.InstructionScheduleMap` is ignored - when it is set to transpile with :class:`.BackendV2` instance. From 20977edb6b3d7c366be1e5f395c11922b7ac1342 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 30 Mar 2023 15:34:15 +0900 Subject: [PATCH 10/14] More strict gate uniformity check when create from schedules. --- qiskit/transpiler/target.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 6b60a338bcd8..7516f6882103 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -39,6 +39,7 @@ from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.utils.deprecation import deprecate_arguments +from qiskit.exceptions import QiskitError # import QubitProperties here to provide convenience alias for building a # full target @@ -450,6 +451,7 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err qiskit_inst_name_map.update(inst_name_map) for inst_name in inst_map.instructions: + # Prepare dictionary of instruction properties out_props = {} for qargs in inst_map.qubits_with_instruction(inst_name): try: @@ -486,7 +488,9 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err out_props[qargs] = props if not out_props: continue + # Prepare Qiskit Gate object assigned to the entries if inst_name not in self._gate_map: + # Entry not found: Add new instruction if inst_name in qiskit_inst_name_map: # Remove qargs with length that doesn't match with instruction qubit number inst_obj = qiskit_inst_name_map[inst_name] @@ -497,20 +501,31 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err normalized_props[qargs] = prop self.add_instruction(inst_obj, normalized_props, name=inst_name) else: - # Assumes qubit number and parameter names are consistent - qargs0 = inst_map.qubits_with_instruction(inst_name)[0] - try: - qargs0 = tuple(qargs0) - except TypeError: - qargs0 = (qargs0,) - cal = getattr(out_props[qargs0], "_calibration") + # Check qubit length parameter name uniformity. + qlen = set() + param_names = set() + for qargs in inst_map.qubits_with_instruction(inst_name): + if isinstance(qargs, int): + qargs = (qargs, ) + qlen.add(len(qargs)) + cal = getattr(out_props[tuple(qargs)], "_calibration") + param_names.add(tuple(cal.get_signature().parameters.keys())) + if len(qlen) > 1 or len(param_names) > 1: + raise QiskitError( + f"Schedules for {inst_name} are defined non-uniformly for " + f"multiple qubit lengths {qlen}, " + f"or different parameter names {param_names}. " + "Provide these schedules with inst_name_map or define them with " + "different names for different gate parameters." + ) inst_obj = Gate( name=inst_name, - num_qubits=len(qargs0), - params=list(map(Parameter, cal.get_signature().parameters.keys())), + num_qubits=next(iter(qlen)), + params=list(map(Parameter, next(iter(param_names)))), ) self.add_instruction(inst_obj, out_props, name=inst_name) else: + # Entry found: Update "existing" instructions. for qargs, prop in out_props.items(): if qargs not in self._gate_map[inst_name]: continue From f203f59399989bc7998a94269f837dc2149ad763 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 30 Mar 2023 16:00:54 +0900 Subject: [PATCH 11/14] Added note for calibration behavior --- qiskit/transpiler/target.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 7516f6882103..de25fff3e836 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -84,7 +84,32 @@ def __init__( @property def calibration(self): - """The pulse representation of the instruction.""" + """The pulse representation of the instruction. + + .. note:: + + This attribute always returns a Qiskit pulse program, but it is internally + wrapped by the :class:`.CalibrationEntry` to manage unbound parameters + and to uniformly handle different data representation, + for example, un-parsed Pulse Qobj JSON that a backend provider may provide. + + This value can be overridden through the property setter in following manner. + When you set either :class:`.Schedule` or :class:`.ScheduleBlock` this is + always treated as a user-defined (custom) calibration and + the transpiler may automatically attach the calibration data to the output circuit. + This calibration data may appear in the wire format as an inline calibration, + which may further update the backend standard instruction set architecture. + + If you are a backend provider who provides a default calibration data + that is not needed to be attached to the transpiled quantum circuit, + you can directly set :class:`.CalibrationEntry` instance to this attribute, + in which you should set :code:`user_provided=False` when you define + calibration data for the entry. End users can still intentionally utilize + the calibration data, for example, to run pulse-level simulation of the circuit. + However, such entry doesn't appear in the wire format, and backend must + use own definition to compile the circuit down to the execution format. + + """ if self._calibration is None: return None return self._calibration.get_schedule() @@ -421,7 +446,7 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err """Update the target from an instruction schedule map. If the input instruction schedule map contains new instructions not in - the target they will be added. However if it contains additional qargs + the target they will be added. However, if it contains additional qargs for an existing instruction in the target it will error. Args: From 2c4b6873206640682dd15a956041ac520a6bf546 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 30 Mar 2023 16:25:31 +0900 Subject: [PATCH 12/14] More documentation for CalibrationEntry --- qiskit/pulse/calibration_entries.py | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py index 1aa64b8779b7..6564dab1e193 100644 --- a/qiskit/pulse/calibration_entries.py +++ b/qiskit/pulse/calibration_entries.py @@ -31,7 +31,31 @@ class CalibrationPublisher(IntEnum): class CalibrationEntry(metaclass=ABCMeta): - """A metaclass of a calibration entry.""" + """A metaclass of a calibration entry. + + This class defines a standard model of Qiskit pulse program that is + agnostic to the underlying in-memory representation. + + This entry distinguishes whether this is provided by end-users or a backend + by :attr:`.user_provided` attribute which may be provided when + the actual calibration data is provided to the entry with by :meth:`define`. + + Note that a custom entry provided by an end-user may appear in the wire-format + as an inline calibration, e.g. :code:`defcal` of the QASM3, + that may update the backend instruction set architecture for execution. + + .. note:: + + This and built-in subclasses are expected to be private without stable user-facing API. + The purpose of this class is to wrap different + in-memory pulse program representations in Qiskit, so that it can provide + the standard data model and API which are primarily used by the transpiler ecosystem. + It is assumed that end-users will never directly instantiate this class, + but :class:`.Target` or :class:`.InstructionScheduleMap` internally use this data model + to avoid implementing a complicated branching logic to + manage different calibration data formats. + + """ @abstractmethod def define(self, definition: Any, user_provided: bool): @@ -40,6 +64,8 @@ def define(self, definition: Any, user_provided: bool): Args: definition: Definition of this entry. user_provided: If this entry is defined by user. + If the flag is set, this calibration may appear in the wire format + as an inline calibration, to override the backend instruction set architecture. """ pass @@ -56,6 +82,10 @@ def get_signature(self) -> inspect.Signature: def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]: """Generate schedule from entry definition. + If the pulse program is templated with :class:`.Parameter` objects, + you can provide corresponding parameter values for this method + to get a particular pulse program with assigned parameters. + Args: args: Command parameters. kwargs: Command keyword parameters. @@ -78,6 +108,10 @@ class ScheduleDef(CalibrationEntry): A pulse schedule must provide signature with the .parameters attribute. This entry can be parameterized by a Qiskit Parameter object. The .get_schedule method returns a parameter-assigned pulse program. + + .. see_also:: + :class:`.CalibrationEntry` for the purpose of this class. + """ def __init__(self, arguments: Optional[Sequence[str]] = None): @@ -190,6 +224,10 @@ class CallableDef(CalibrationEntry): provide the signature. This entry is parameterized by the function signature and .get_schedule method returns a non-parameterized pulse program by consuming the provided arguments and keyword arguments. + + .. see_also:: + :class:`.CalibrationEntry` for the purpose of this class. + """ def __init__(self): @@ -248,6 +286,10 @@ class PulseQobjDef(ScheduleDef): the provided qobj converter. Because the Qobj JSON doesn't provide signature, conversion process occurs when the signature is requested for the first time and the generated pulse program is cached for performance. + + .. see_also:: + :class:`.CalibrationEntry` for the purpose of this class. + """ def __init__( From f217d0197fff061ec6a94cd4212af073fb020626 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 30 Mar 2023 16:59:36 +0900 Subject: [PATCH 13/14] Add logic to prevent unintentional backend mutation with instmap. --- qiskit/compiler/transpiler.py | 3 ++ .../python/transpiler/test_pulse_gate_pass.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 69dc89e79e40..c4d78b89d0a8 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -13,6 +13,7 @@ # pylint: disable=invalid-sequence-index """Circuit transpile function""" +import copy import io from itertools import cycle import logging @@ -664,6 +665,8 @@ def _parse_transpile_args( durations = _parse_instruction_durations(backend, instruction_durations, dt, circuits) timing_constraints = _parse_timing_constraints(backend, timing_constraints, num_circuits) if inst_map is not None and inst_map.has_custom_gate() and target is not None: + # Do not mutate backend target + target = copy.deepcopy(target) target.update_from_instruction_schedule_map(inst_map) if scheduling_method and any(d is None for d in durations): raise TranspilerError( diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index 0ada1e03a04e..b5f8c03c4711 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -374,3 +374,34 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level) } } self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_instmap_not_mutate_backend(self): + """Do not override default backend target when transpile with inst map. + + Providing an instmap for the transpile arguments may override target, + which might be pulled from the provided backend instance. + This should not override the source object since the same backend may + be used for future transpile without intention of instruction overriding. + """ + backend = FakeAthensV2() + original_sx0 = backend.target["sx"][(0,)].calibration + + instmap = FakeAthens().defaults().instruction_schedule_map + instmap.add("sx", (0,), self.custom_sx_q0) + + qc = circuit.QuantumCircuit(1) + qc.sx(0) + qc.measure_all() + + transpiled_qc = transpile( + qc, + FakeAthensV2(), + inst_map=instmap, + initial_layout=[0], + ) + self.assertTrue(transpiled_qc.has_calibration_for(transpiled_qc.data[0])) + + self.assertEqual( + backend.target["sx"][(0,)].calibration, + original_sx0, + ) From d0b2109ae08fe342be5ea4783ea49e84d1a96a2e Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 30 Mar 2023 17:13:37 +0900 Subject: [PATCH 14/14] fix lint --- qiskit/transpiler/target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index de25fff3e836..27b9f8fa57af 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -531,7 +531,7 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err param_names = set() for qargs in inst_map.qubits_with_instruction(inst_name): if isinstance(qargs, int): - qargs = (qargs, ) + qargs = (qargs,) qlen.add(len(qargs)) cal = getattr(out_props[tuple(qargs)], "_calibration") param_names.add(tuple(cal.get_signature().parameters.keys()))