From 183e859ad45b6608751c91505eaef09688e85b90 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Fri, 2 Dec 2022 00:45:24 +0900 Subject: [PATCH] Fix add_calibration bug --- qiskit/circuit/quantumcircuit.py | 27 +++++++++++++++-- qiskit/dagcircuit/dagcircuit.py | 29 ++++++++++++++++--- ...eterized-calibration-f5c0a740fcdeb2ec.yaml | 10 +++++++ .../python/transpiler/test_pulse_gate_pass.py | 22 ++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4348cc8ae2b9..3cdab921588f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4603,10 +4603,33 @@ def add_calibration( Raises: Exception: if the gate is of type string and params is None. """ + + def _format(operand): + try: + # Using float/complex value as a dict key is not good idea. + # This makes the mapping quite sensitive to the rounding error. + # However, the mechanism is already tied to the execution model (i.e. pulse gate) + # and we cannot easily update this rule. + # The same logic exists in DAGCircuit.add_calibration. + evaluated = complex(operand) + if np.isreal(evaluated): + evaluated = float(evaluated.real) + if evaluated.is_integer(): + evaluated = int(evaluated) + return evaluated + except TypeError: + # Unassigned parameter + return operand + if isinstance(gate, Gate): - self._calibrations[gate.name][(tuple(qubits), tuple(gate.params))] = schedule + params = gate.params + gate = gate.name + if params is not None: + params = tuple(map(_format, params)) else: - self._calibrations[gate][(tuple(qubits), tuple(params or []))] = schedule + params = tuple() + + self._calibrations[gate][(tuple(qubits), params)] = schedule # Functions only for scheduled circuits def qubit_duration(self, *qubits: Union[Qubit, int]) -> float: diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 60aa846b3dac..0368bb8df7c2 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -161,12 +161,33 @@ def add_calibration(self, gate, qubits, schedule, params=None): Raises: Exception: if the gate is of type string and params is None. """ + + def _format(operand): + try: + # Using float/complex value as a dict key is not good idea. + # This makes the mapping quite sensitive to the rounding error. + # However, the mechanism is already tied to the execution model (i.e. pulse gate) + # and we cannot easily update this rule. + # The same logic exists in QuantumCircuit.add_calibration. + evaluated = complex(operand) + if np.isreal(evaluated): + evaluated = float(evaluated.real) + if evaluated.is_integer(): + evaluated = int(evaluated) + return evaluated + except TypeError: + # Unassigned parameter + return operand + if isinstance(gate, Gate): - self._calibrations[gate.name][ - (tuple(qubits), tuple(float(p) for p in gate.params)) - ] = schedule + params = gate.params + gate = gate.name + if params is not None: + params = tuple(map(_format, params)) else: - self._calibrations[gate][(tuple(qubits), tuple(params or []))] = schedule + params = tuple() + + self._calibrations[gate][(tuple(qubits), params)] = schedule def has_calibration_for(self, node): """Return True if the dag has a calibration defined for the node operation. In this diff --git a/releasenotes/notes/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml b/releasenotes/notes/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml new file mode 100644 index 000000000000..64ae4246b4af --- /dev/null +++ b/releasenotes/notes/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + :meth:`.QuantumCircuit.add_calibrations` and :meth:`.DAGCircuit.add_calibrations` + method have been updated to resolve the mismatch of parameter formatting logic. + So far one of DAGCircuit tried to typecast every parameter into float, + while QuantumCircuit used given parameters as-is. + This has been crashing transpile when the pulse gate to assign + was kept parameterized throughout the all transpile steps. + Both methods now have the identical logic to format the gate parameters. diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index b6a227f6a724..1979340c2c6a 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -139,6 +139,28 @@ def test_transpile_with_custom_gate(self): } self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + def test_transpile_with_parameterized_custom_gate(self): + """Test providing non-basis gate, which is kept parameterized throughout transpile.""" + backend = FakeAthens() + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) + + param = circuit.Parameter("new_P0") + qc = circuit.QuantumCircuit(1) + qc.append(circuit.Gate("my_gate", 1, [param]), [0]) + + transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) + + my_gate_q0_p = self.my_gate_q0.assign_parameters({self.sched_param: param}, inplace=False) + + ref_calibration = { + "my_gate": { + ((0,), (param,)): my_gate_q0_p, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + def test_transpile_with_multiple_circuits(self): """Test transpile with multiple circuits with custom gate.""" backend = FakeAthens()