Repository: Qiskit/qiskit — pull request #10834 (python)
Title: Allow dynamical decoupling pass to work on circuits with pulse gates 

### Summary\r\n\r\nAllow dynamical decoupling pass to work on circuits with pulse gates \r\n\r\n### Details and comments\r\n\r\nThe pass was accessing the circuit calibrations incorrectly, resulting in\r\nan error when one of the gates in the dynamical decoupling sequence had\r\na pulse gate calibration.\r\n\r\nCloses #10833

Resolved issues:
title: Dynamical decoupling fails when a gate in the decoupling sequence has a calibration attached to the circuit
body: ### Environment\n\n- **Qiskit Terra version**: 0.25.1\r\n- **Python version**: 3.10\r\n- **Operating system**: Fedora Linux\r\n\n\n### What is happening?\n\n`PadDynamicalDecoupling` raises `TypeError: unhashable type: 'list'` when run on a circuit with a calibration for one of the gates in the decoupling sequence.\n\n### How can we reproduce the issue?\n\n```python\r\nfrom qiskit_ibm_provider import IBMProvider\r\n\r\n\r\nprovider = IBMProvider()\r\nbackend = provider.get_backend("ibm_nazca")\r\n\r\n\r\nimport numpy as np\r\n\r\nfrom qiskit import pulse, QuantumCircuit\r\nfrom qiskit.transpiler import PassManager\r\nfrom qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling\r\nfrom qiskit.circuit.library import XGate, YGate\r\nfrom qiskit.pulse import DriveChannel\r\n\r\n\r\ninst_sched_map = backend.instruction_schedule_map\r\n\r\n\r\ncirc = QuantumCircuit(1, 1)\r\ncirc.x(0)\r\ncirc.delay(1600, 0)\r\ncirc.x(0)\r\n\r\nfor qubit in range(backend.configuration().n_qubits):\r\n    with pulse.build(f"y gate for qubit {qubit}") as sched:\r\n        with pulse.phase_offset(np.pi / 2, DriveChannel(qubit)):\r\n            x_sched = inst_sched_map.get("x", qubits=[qubit])\r\n            pulse.call(x_sched)\r\n        circ.add_calibration("y", [qubit], sched)\r\n\r\ndd_sequence = [XGate(), YGate()]*4\r\ndurations = backend.target.durations()\r\n\r\npm = PassManager(\r\n        [\r\n            ALAPScheduleAnalysis(durations),\r\n            PadDynamicalDecoupling(\r\n                durations,\r\n                dd_sequence,\r\n                pulse_alignment=backend.configuration().timing_constraints[\r\n                    "pulse_alignment"\r\n                ],\r\n            ),\r\n        ]\r\n    )\r\n\r\npm.run(circ)\r\n```\n\n### What should happen?\n\nThe duration of the gate calibration should be used to schedule the decoupling sequence.\n\n### Any suggestions?\n\nA PR with a fix will be opened shortly.

Hints:

Fix patch:
diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\nindex bbb1b39bbd72..fb33361a4959 100644\n--- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\n+++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\n@@ -16,7 +16,7 @@\n import logging\n import numpy as np\n \n-from qiskit.circuit import Qubit, Gate\n+from qiskit.circuit import Gate, ParameterExpression, Qubit\n from qiskit.circuit.delay import Delay\n from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate\n from qiskit.circuit.reset import Reset\n@@ -227,7 +227,8 @@ def _pre_runhook(self, dag: DAGCircuit):\n             for gate in self._dd_sequence:\n                 try:\n                     # Check calibration.\n-                    gate_length = dag.calibrations[gate.name][(physical_index, gate.params)]\n+                    params = self._resolve_params(gate)\n+                    gate_length = dag.calibrations[gate.name][((physical_index,), params)].duration\n                     if gate_length % self._alignment != 0:\n                         # This is necessary to implement lightweight scheduling logic for this pass.\n                         # Usually the pulse alignment constraint and pulse data chunk size take\n@@ -394,6 +395,17 @@ def _constrained_length(values):\n \n         dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase)\n \n+    @staticmethod\n+    def _resolve_params(gate: Gate) -> tuple:\n+        """Return gate params with any bound parameters replaced with floats"""\n+        params = []\n+        for p in gate.params:\n+            if isinstance(p, ParameterExpression) and not p.parameters:\n+                params.append(float(p))\n+            else:\n+                params.append(p)\n+        return tuple(params)\n+\n     @staticmethod\n     def _mod_2pi(angle: float, atol: float = 0):\n         """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π"""\ndiff --git a/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml\nnew file mode 100644\nindex 000000000000..cdf23b9ef9a3\n--- /dev/null\n+++ b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml\n@@ -0,0 +1,7 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in the :class:`.PadDynamicalDecoupling` transpiler pass which\n+    would cause the pass to fail if a circuit contained a pulse gate\n+    calibration for one of the gates in the decoupling sequence. Fixed `#10833\n+    <https://github.com/Qiskit/qiskit/issues/10833>`_.\n

Test patch:
diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py\nindex bd8c44ab0973..6ae6a7b3c311 100644\n--- a/test/python/transpiler/test_dynamical_decoupling.py\n+++ b/test/python/transpiler/test_dynamical_decoupling.py\n@@ -17,7 +17,8 @@\n from numpy import pi\n from ddt import ddt, data\n \n-from qiskit.circuit import QuantumCircuit, Delay, Measure, Reset, Parameter\n+from qiskit import pulse\n+from qiskit.circuit import Gate, QuantumCircuit, Delay, Measure, Reset, Parameter\n from qiskit.circuit.library import XGate, YGate, RXGate, UGate, CXGate, HGate\n from qiskit.quantum_info import Operator\n from qiskit.transpiler.instruction_durations import InstructionDurations\n@@ -29,7 +30,6 @@\n from qiskit.transpiler.passmanager import PassManager\n from qiskit.transpiler.exceptions import TranspilerError\n from qiskit.transpiler.target import Target, InstructionProperties\n-from qiskit import pulse\n \n from qiskit.test import QiskitTestCase\n \n@@ -339,6 +339,154 @@ def test_insert_dd_ghz_everywhere(self):\n \n         self.assertEqual(ghz4_dd, expected)\n \n+    def test_insert_dd_with_pulse_gate_calibrations(self):\n+        """Test DD gates are inserted without error when circuit calibrations are used\n+\n+                      ┌───┐            ┌───────────────┐      ┌───┐       »\n+           q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────»\n+                ┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐»\n+           q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├»\n+                ├───────────────┴┐└───┘      ┌─┴─┐      └────────────────┘»\n+           q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────»\n+                ├────────────────┤           └───┘            ┌─┴─┐       »\n+           q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────»\n+                └────────────────┘                            └───┘       »\n+        meas: 4/══════════════════════════════════════════════════════════»\n+                                                                           »\n+        «        ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐\n+        «   q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├─────────\n+        «        └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐\n+        «   q_1: ─────────────────────────────────────────░──╫─┤M├──────\n+        «                                                 ░  ║ └╥┘┌─┐\n+        «   q_2: ─────────────────────────────────────────░──╫──╫─┤M├───\n+        «                                                 ░  ║  ║ └╥┘┌─┐\n+        «   q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├\n+        «                                                 ░  ║  ║  ║ └╥┘\n+        «meas: 4/════════════════════════════════════════════╩══╩══╩══╩═\n+        «                                                     0  1  2  3\n+        """\n+        dd_sequence = [XGate(), XGate()]\n+        pm = PassManager(\n+            [\n+                ALAPScheduleAnalysis(self.durations),\n+                PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]),\n+            ]\n+        )\n+\n+        # Change duration to 100 from the 50 in self.durations to make sure\n+        # gate duration is used correctly.\n+        with pulse.builder.build() as x_sched:\n+            pulse.builder.delay(100, pulse.DriveChannel(0))\n+\n+        circ_in = self.ghz4.measure_all(inplace=False)\n+        circ_in.add_calibration(XGate(), (0,), x_sched)\n+\n+        ghz4_dd = pm.run(circ_in)\n+\n+        expected = self.ghz4.copy()\n+        expected = expected.compose(Delay(50), [1], front=True)\n+        expected = expected.compose(Delay(750), [2], front=True)\n+        expected = expected.compose(Delay(950), [3], front=True)\n+\n+        # Delays different from those of the default case using self.durations\n+        expected = expected.compose(Delay(75), [0])\n+        expected = expected.compose(XGate(), [0])\n+        expected = expected.compose(Delay(150), [0])\n+        expected = expected.compose(XGate(), [0])\n+        expected = expected.compose(Delay(75), [0])\n+\n+        expected = expected.compose(Delay(300), [1])\n+\n+        expected.measure_all()\n+        expected.add_calibration(XGate(), (0,), x_sched)\n+\n+        self.assertEqual(ghz4_dd, expected)\n+\n+    def test_insert_dd_with_pulse_gate_calibrations_with_parmas(self):\n+        """Test DD gates are inserted without error when parameterized circuit calibrations are used\n+\n+                      ┌───┐            ┌───────────────┐      ┌───┐       »\n+           q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────»\n+                ┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐»\n+           q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├»\n+                ├───────────────┴┐└───┘      ┌─┴─┐      └────────────────┘»\n+           q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────»\n+                ├────────────────┤           └───┘            ┌─┴─┐       »\n+           q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────»\n+                └────────────────┘                            └───┘       »\n+        meas: 4/══════════════════════════════════════════════════════════»\n+                                                                           »\n+        «        ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐\n+        «   q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├─────────\n+        «        └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐\n+        «   q_1: ─────────────────────────────────────────░──╫─┤M├──────\n+        «                                                 ░  ║ └╥┘┌─┐\n+        «   q_2: ─────────────────────────────────────────░──╫──╫─┤M├───\n+        «                                                 ░  ║  ║ └╥┘┌─┐\n+        «   q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├\n+        «                                                 ░  ║  ║  ║ └╥┘\n+        «meas: 4/════════════════════════════════════════════╩══╩══╩══╩═\n+        «                                                     0  1  2  3\n+        """\n+        # Change duration to 100 from the 50 in self.durations to make sure\n+        # gate duration is used correctly.\n+        amp = Parameter("amp")\n+        with pulse.builder.build() as sched:\n+            pulse.builder.play(\n+                pulse.Gaussian(100, amp=amp, sigma=10.0),\n+                pulse.DriveChannel(0),\n+            )\n+\n+        class Echo(Gate):\n+            """Dummy Gate subclass for testing\n+\n+            In this test, we use a non-standard gate so we can add parameters\n+            to it, in order to test the handling of parameters by\n+            PadDynamicalDecoupling. PadDynamicalDecoupling checks that the DD\n+            sequence is equivalent to the identity, so we can not use Gate\n+            directly. Here we subclass Gate and add the identity as its matrix\n+            representation to satisfy PadDynamicalDecoupling's check.\n+            """\n+\n+            def __array__(self, dtype=None):\n+                return np.eye(2, dtype=dtype)\n+\n+        # A gate with one unbound and one bound parameter to leave in the final\n+        # circuit.\n+        echo = Echo("echo", 1, [amp, 10.0])\n+\n+        circ_in = self.ghz4.measure_all(inplace=False)\n+        circ_in.add_calibration(echo, (0,), sched)\n+\n+        dd_sequence = [echo, echo]\n+        pm = PassManager(\n+            [\n+                ALAPScheduleAnalysis(self.durations),\n+                PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]),\n+            ]\n+        )\n+\n+        ghz4_dd = pm.run(circ_in)\n+\n+        expected = self.ghz4.copy()\n+        expected = expected.compose(Delay(50), [1], front=True)\n+        expected = expected.compose(Delay(750), [2], front=True)\n+        expected = expected.compose(Delay(950), [3], front=True)\n+\n+        # Delays different from those of the default case using self.durations\n+        expected = expected.compose(Delay(75), [0])\n+        expected = expected.compose(echo, [0])\n+        expected = expected.compose(Delay(150), [0])\n+        expected = expected.compose(echo, [0])\n+        expected = expected.compose(Delay(75), [0])\n+\n+        expected = expected.compose(Delay(300), [1])\n+\n+        expected.measure_all()\n+        expected.add_calibration(echo, (0,), sched)\n+\n+        self.assertEqual(ghz4_dd, expected)\n+\n     def test_insert_dd_ghz_xy4(self):\n         """Test XY4 sequence of DD gates.\n \n