Repository: Qiskit/qiskit

Issue:
Avoid circuit<->dag conversion overhead in basis translator
### What should we add?

Right now in the basis translator we spend a large portion of time (roughly 1/3rd of the typical runtime) converting back and forth between `QuantumCircuit` and `DAGCircuit`. This is mostly of this is to support calling `assign_parameters()` which only exists on a circuit, but the transpiler only works with DAGCircuit. This ends up being fairly high overhead because we deepcopy instructions multiple times as we go back and forth

We should be able to avoid all this overhead by doing parameter assignment directly in the `DAGCircuit`  which would probably require porting the equivalent of `ParameterTable` and the matching assignment functions to `DAGCircuit` (or potentially bypassing the traditional parameter assignment mechanisms as we don't need the full flexibility for this).


Discussion:
Some thoughts, copied from #10275:

I'm not certain that giving `DAGCircuit` the ability to assign parameters is going to be the cleanest way forwards to improving this.  The way `QuantumCircuit` manages its `ParameterTable` is only realistically possible because `QuantumCircuit` is (nearly) an additive-only interface with a single call point that's allowed to add instructions to a circuit (`QuantumCircuit._append`, give or take).  `DAGCircuit` has many many methods to add potentially parametrised instructions, but further, it's got a lot of methods that can replace operations with arbitrary other blocks that would also need updating.  The root of the issue is that `ParameterTable`+`QuantumCircuit` has a strong requirement on data coherence in a way that it'll be very tricky and bug-prone to do in `DAGCircuit`.

Resolving patch:
diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py
--- a/qiskit/transpiler/passes/basis/basis_translator.py
+++ b/qiskit/transpiler/passes/basis/basis_translator.py
@@ -22,7 +22,14 @@
 
 import rustworkx
 
-from qiskit.circuit import Gate, ParameterVector, QuantumRegister, ControlFlowOp, QuantumCircuit
+from qiskit.circuit import (
+    Gate,
+    ParameterVector,
+    QuantumRegister,
+    ControlFlowOp,
+    QuantumCircuit,
+    ParameterExpression,
+)
 from qiskit.dagcircuit import DAGCircuit
 from qiskit.converters import circuit_to_dag, dag_to_circuit
 from qiskit.circuit.equivalence import Key, NodeData
@@ -294,17 +301,71 @@ def _replace_node(self, dag, node, instr_map):
                     node.op.params, node.op.name, target_params, target_dag
                 )
             )
-
         if node.op.params:
-            # Convert target to circ and back to assign_parameters, since
-            # DAGCircuits won't have a ParameterTable.
-            target_circuit = dag_to_circuit(target_dag)
-
-            target_circuit.assign_parameters(
-                dict(zip_longest(target_params, node.op.params)), inplace=True
-            )
+            parameter_map = dict(zip(target_params, node.op.params))
+            bound_target_dag = target_dag.copy_empty_like()
+            for inner_node in target_dag.topological_op_nodes():
+                if any(isinstance(x, ParameterExpression) for x in inner_node.op.params):
+                    new_op = inner_node.op.copy()
+                    new_params = []
+                    for param in new_op.params:
+                        if not isinstance(param, ParameterExpression):
+                            new_params.append(param)
+                        else:
+                            bind_dict = {x: parameter_map[x] for x in param.parameters}
+                            if any(isinstance(x, ParameterExpression) for x in bind_dict.values()):
+                                new_value = param
+                                for x in bind_dict.items():
+                                    new_value = new_value.assign(*x)
+                            else:
+                                new_value = param.bind(bind_dict)
+                            # cast from ParameterExpression to number, if no parameters left
+                            if not new_value.parameters:
+                                if new_value.is_real():
+                                    new_value = (
+                                        int(new_value)
+                                        if new_value._symbol_expr.is_integer
+                                        else float(new_value)
+                                    )
+                                else:
+                                    new_value = complex(new_value)
+                            new_params.append(new_value)
+                    new_op.params = new_params
+                else:
+                    new_op = inner_node.op
+                bound_target_dag.apply_operation_back(new_op, inner_node.qargs, inner_node.cargs)
+            if isinstance(target_dag.global_phase, ParameterExpression):
+                old_phase = target_dag.global_phase
+                bind_dict = {x: parameter_map[x] for x in old_phase.parameters}
+                if any(isinstance(x, ParameterExpression) for x in bind_dict.values()):
+                    new_phase = old_phase
+                    for x in bind_dict.items():
+                        new_phase = new_phase.assign(*x)
 
-            bound_target_dag = circuit_to_dag(target_circuit)
+                else:
+                    new_phase = old_phase.bind(bind_dict)
+                if not new_phase.parameters:
+                    if new_phase.is_real():
+                        new_phase = (
+                            int(new_phase)
+                            if new_phase._symbol_expr.is_integer
+                            else float(new_phase)
+                        )
+                    else:
+                        # If is_real() evals false try casting to a float
+                        # anyway in case there is a rounding error adding
+                        # a near 0 complex term
+                        try:
+                            new_phase = float(new_phase)
+                        except TypeError as exc:
+                            raise TranspilerError(
+                                f"Global phase: {new_phase} is complex which is invalid"
+                            ) from exc
+                try:
+                    new_phase = float(new_phase)
+                except TypeError:
+                    pass
+                bound_target_dag.global_phase = new_phase
         else:
             bound_target_dag = target_dag
 


Test patch: