Repository: Qiskit/qiskit (python)

Issue:
Dynamical decoupling fails when a gate in the decoupling sequence has a calibration attached to the circuit
### Environment

- **Qiskit Terra version**: 0.25.1
- **Python version**: 3.10
- **Operating system**: Fedora Linux


### What is happening?

`PadDynamicalDecoupling` raises `TypeError: unhashable type: 'list'` when run on a circuit with a calibration for one of the gates in the decoupling sequence.

### How can we reproduce the issue?

```python
from qiskit_ibm_provider import IBMProvider


provider = IBMProvider()
backend = provider.get_backend("ibm_nazca")


import numpy as np

from qiskit import pulse, QuantumCircuit
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling
from qiskit.circuit.library import XGate, YGate
from qiskit.pulse import DriveChannel


inst_sched_map = backend.instruction_schedule_map


circ = QuantumCircuit(1, 1)
circ.x(0)
circ.delay(1600, 0)
circ.x(0)

for qubit in range(backend.configuration().n_qubits):
    with pulse.build(f"y gate for qubit {qubit}") as sched:
        with pulse.phase_offset(np.pi / 2, DriveChannel(qubit)):
            x_sched = inst_sched_map.get("x", qubits=[qubit])
            pulse.call(x_sched)
        circ.add_calibration("y", [qubit], sched)

dd_sequence = [XGate(), YGate()]*4
durations = backend.target.durations()

pm = PassManager(
        [
            ALAPScheduleAnalysis(durations),
            PadDynamicalDecoupling(
                durations,
                dd_sequence,
                pulse_alignment=backend.configuration().timing_constraints[
                    "pulse_alignment"
                ],
            ),
        ]
    )

pm.run(circ)
```

### What should happen?

The duration of the gate calibration should be used to schedule the decoupling sequence.

### Any suggestions?

A PR with a fix will be opened shortly.

Pull request description:
Allow dynamical decoupling pass to work on circuits with pulse gates 
### Summary

Allow dynamical decoupling pass to work on circuits with pulse gates 

### Details and comments

The pass was accessing the circuit calibrations incorrectly, resulting in
an error when one of the gates in the dynamical decoupling sequence had
a pulse gate calibration.

Closes #10833

Patch:
diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py
index 75d51ed59..3e347b0be 100644
--- a/qiskit/circuit/parameter.py
+++ b/qiskit/circuit/parameter.py
@@ -90,6 +90,24 @@ class Parameter(ParameterExpression):
             symbol = symengine.Symbol(name)
         super().__init__(symbol_map={self: symbol}, expr=symbol)
 
+    def assign(self, parameter, value):
+        if parameter != self:
+            # Corresponds to superclass calls to `subs` and `bind` that would implicitly set
+            # `allow_unknown_parameters=False`.
+            raise CircuitError(
+                f"Cannot bind Parameters ({[str(parameter)]}) not present in expression."
+            )
+        if isinstance(value, ParameterExpression):
+            # This is the `super().subs` case.
+            return value
+        # This is the `super().bind` case, where we're required to return a `ParameterExpression`,
+        # so we need to lift the given value to a symbolic expression.
+        if _optionals.HAS_SYMENGINE:
+            from symengine import sympify
+        else:
+            from sympy import sympify
+        return ParameterExpression({}, sympify(value))
+
     def subs(self, parameter_map: dict, allow_unknown_parameters: bool = False):
         """Substitute self with the corresponding parameter in ``parameter_map``."""
         if self in parameter_map:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
index 77660a558..6e01f3f32 100644
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -2523,14 +2523,26 @@ class QuantumCircuit:
     @property
     def num_parameters(self) -> int:
         """The number of parameter objects in the circuit."""
+        # Avoid a (potential) object creation if we can.
+        if self._parameters is not None:
+            return len(self._parameters)
         return len(self._unsorted_parameters())
 
     def _unsorted_parameters(self) -> set[Parameter]:
-        """Efficiently get all parameters in the circuit, without any sorting overhead."""
-        parameters = set(self._parameter_table)
-        if isinstance(self.global_phase, ParameterExpression):
-            parameters.update(self.global_phase.parameters)
+        """Efficiently get all parameters in the circuit, without any sorting overhead.
+
+        .. warning::
 
+            The returned object may directly view onto the ``ParameterTable`` internals, and so
+            should not be mutated.  This is an internal performance detail.  Code outside of this
+            package should not use this method.
+        """
+        # This should be free, by accessing the actual backing data structure of the table, but that
+        # means that we need to copy it if adding keys from the global phase.
+        parameters = self._parameter_table.get_keys()
+        if isinstance(self.global_phase, ParameterExpression):
+            # Deliberate copy.
+            parameters = parameters | self.global_phase.parameters
         return parameters
 
     @overload
@@ -2648,6 +2660,7 @@ class QuantumCircuit:
         # 'target' so we can take advantage of any caching we might be doing.
         if isinstance(parameters, dict):
             raw_mapping = parameters if flat_input else self._unroll_param_dict(parameters)
+            # Remember that we _must not_ mutate the output of `_unsorted_parameters`.
             our_parameters = self._unsorted_parameters()
             if strict and (extras := raw_mapping.keys() - our_parameters):
                 raise CircuitError(
diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py
index bbb1b39bb..fb33361a4 100644
--- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py
+++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py
@@ -16,7 +16,7 @@ from __future__ import annotations
 import logging
 import numpy as np
 
-from qiskit.circuit import Qubit, Gate
+from qiskit.circuit import Gate, ParameterExpression, Qubit
 from qiskit.circuit.delay import Delay
 from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate
 from qiskit.circuit.reset import Reset
@@ -227,7 +227,8 @@ class PadDynamicalDecoupling(BasePadding):
             for gate in self._dd_sequence:
                 try:
                     # Check calibration.
-                    gate_length = dag.calibrations[gate.name][(physical_index, gate.params)]
+                    params = self._resolve_params(gate)
+                    gate_length = dag.calibrations[gate.name][((physical_index,), params)].duration
                     if gate_length % self._alignment != 0:
                         # This is necessary to implement lightweight scheduling logic for this pass.
                         # Usually the pulse alignment constraint and pulse data chunk size take
@@ -394,6 +395,17 @@ class PadDynamicalDecoupling(BasePadding):
 
         dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase)
 
+    @staticmethod
+    def _resolve_params(gate: Gate) -> tuple:
+        """Return gate params with any bound parameters replaced with floats"""
+        params = []
+        for p in gate.params:
+            if isinstance(p, ParameterExpression) and not p.parameters:
+                params.append(float(p))
+            else:
+                params.append(p)
+        return tuple(params)
+
     @staticmethod
     def _mod_2pi(angle: float, atol: float = 0):
         """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π"""
diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py
index 66e172b1e..ad4b8e070 100644
--- a/qiskit/visualization/circuit/latex.py
+++ b/qiskit/visualization/circuit/latex.py
@@ -536,7 +536,7 @@ class QCircuitImage:
             register = get_bit_register(self._circuit, node.cargs[0])
             if register is not None:
                 wire2 = self._wire_map[register]
-                idx_str = str(node.cargs[0].index)
+                idx_str = str(self._circuit.find_bit(node.cargs[0]).registers[0][1])
             else:
                 wire2 = self._wire_map[node.cargs[0]]
 
diff --git a/qiskit/visualization/timeline/layouts.py b/qiskit/visualization/timeline/layouts.py
index 5c5737c6e..d77284e3e 100644
--- a/qiskit/visualization/timeline/layouts.py
+++ b/qiskit/visualization/timeline/layouts.py
@@ -48,14 +48,10 @@ The function signature of the layout is restricted to:
 
 Arbitrary layout function satisfying the above format can be accepted.
 """
-
-import warnings
-
 from typing import List, Tuple
 import numpy as np
 
 from qiskit import circuit
-from qiskit.visualization.exceptions import VisualizationError
 from qiskit.visualization.timeline import types
 
 
@@ -70,23 +66,9 @@ def qreg_creg_ascending(bits: List[types.Bits]) -> List[types.Bits]:
     Returns:
         Sorted bits.
     """
-    qregs = []
-    cregs = []
-
-    for bit in bits:
-        if isinstance(bit, circuit.Qubit):
-            qregs.append(bit)
-        elif isinstance(bit, circuit.Clbit):
-            cregs.append(bit)
-        else:
-            raise VisualizationError(f"Unknown bit {bit} is provided.")
-
-    with warnings.catch_warnings():
-        warnings.simplefilter("ignore")
-        qregs = sorted(qregs, key=lambda x: x.index, reverse=False)
-        cregs = sorted(cregs, key=lambda x: x.index, reverse=False)
-
-    return qregs + cregs
+    return [x for x in bits if isinstance(x, circuit.Qubit)] + [
+        x for x in bits if isinstance(x, circuit.Clbit)
+    ]
 
 
 def qreg_creg_descending(bits: List[types.Bits]) -> List[types.Bits]:
@@ -100,21 +82,9 @@ def qreg_creg_descending(bits: List[types.Bits]) -> List[types.Bits]:
     Returns:
         Sorted bits.
     """
-    qregs = []
-    cregs = []
-
-    for bit in bits:
-        if isinstance(bit, circuit.Qubit):
-            qregs.append(bit)
-        elif isinstance(bit, circuit.Clbit):
-            cregs.append(bit)
-        else:
-            raise VisualizationError(f"Unknown bit {bit} is provided.")
-
-    qregs = sorted(qregs, key=lambda x: x.index, reverse=True)
-    cregs = sorted(cregs, key=lambda x: x.index, reverse=True)
-
-    return qregs + cregs
+    return [x for x in bits[::-1] if isinstance(x, circuit.Qubit)] + [
+        x for x in bits[::-1] if isinstance(x, circuit.Clbit)
+    ]
 
 
 def time_map_in_dt(time_window: Tuple[int, int]) -> types.HorizontalAxis:
diff --git a/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml
new file mode 100644
index 000000000..cdf23b9ef
--- /dev/null
+++ b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+  - |
+    Fixed a bug in the :class:`.PadDynamicalDecoupling` transpiler pass which
+    would cause the pass to fail if a circuit contained a pulse gate
+    calibration for one of the gates in the decoupling sequence. Fixed `#10833
+    <https://github.com/Qiskit/qiskit/issues/10833>`_.
diff --git a/releasenotes/notes/optimise-parameter-assign-398ed4f2074ca201.yaml b/releasenotes/notes/optimise-parameter-assign-398ed4f2074ca201.yaml
new file mode 100644
index 000000000..1efa86831
--- /dev/null
+++ b/releasenotes/notes/optimise-parameter-assign-398ed4f2074ca201.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    A fast path for :meth:`.Parameter.assign` has been added, which improves the performance of
+    :meth:`.QuantumCircuit.assign_parameters` for the common case of circuits that contain
+    predominantly "expressions" that are actually just single parameters to be assigned later.
diff --git a/releasenotes/notes/optimise-unsorted-parameters-310e5f2420771f49.yaml b/releasenotes/notes/optimise-unsorted-parameters-310e5f2420771f49.yaml
new file mode 100644
index 000000000..2ab9a49af
--- /dev/null
+++ b/releasenotes/notes/optimise-unsorted-parameters-310e5f2420771f49.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    The performance of :meth:`.QuantumCircuit.assign_parameters` when assigning a
+    single parameter of a circuit that involves many parameters has been improved.


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