Repository: Qiskit/qiskit

Issue:
Optimize1qGatesDecomposition doesn't account for ideal gates correctly
### Environment

- **Qiskit Terra version**: 0.25.0 (but also earlier releases and main)
- **Python version**: 3.11
- **Operating system**: Linux


### What is happening?

If a target has ideal gates with all error rates of 0 reported the output from the pass will translate from one gate to another unecessarily.

### How can we reproduce the issue?

```python
from qiskit.transpiler import Target, InstructionProperties
from qiskit.circuit import QuantumCircuit, Parameter
from qiskit.circuit.library import UGate, HGate
from qiskit.compiler import transpile

target = Target(num_qubits=1)
target.add_instruction(HGate(), {(0,): InstructionProperties(error=0)})
target.add_instruction(
    UGate(Parameter("a"), Parameter("b"), Parameter("c")), {(0,): InstructionProperties(error=0)}
)

qc = QuantumCircuit(1)
qc.h(0)

print(transpile(qc, target=target))
```

### What should happen?

The output should remain as `h` because it's in the target and has the same error rate and gate count as `UGate`. While both outputs are valid and the transpiler is doing it's job, in this situation it's a bit unexpected for the pass to translate the gate when the input was still valid and by the error heuristics no better or worse than the translated output.

### Any suggestions?

I think we probably need to add a condition to fallback to the input in the substitution checks if the output of all the synthesis routines are of equal weight.


Discussion:
Following up on the discussion in #10592.
I'd like to work on this. I can't yet guarantee that I'll have the necessary bandwidth in the coming days, but I'll see what I can do!

Resolving patch:
diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py
--- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py
+++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py
@@ -173,10 +173,16 @@ def _substitution_checks(self, dag, old_run, new_circ, basis, qubit):
         # if we're outside of the basis set, we're obligated to logically decompose.
         # if we're outside of the set of gates for which we have physical definitions,
         #    then we _try_ to decompose, using the results if we see improvement.
+        new_error = 0.0
+        old_error = 0.0
+        if not uncalibrated_and_not_basis_p:
+            new_error = self._error(new_circ, qubit)
+            old_error = self._error(old_run, qubit)
+
         return (
             uncalibrated_and_not_basis_p
-            or (uncalibrated_p and self._error(new_circ, qubit) < self._error(old_run, qubit))
-            or math.isclose(self._error(new_circ, qubit)[0], 0)
+            or (uncalibrated_p and new_error < old_error)
+            or (math.isclose(new_error[0], 0) and not math.isclose(old_error[0], 0))
         )
 
     @control_flow.trivial_recurse


Test patch:
diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py
--- a/test/python/transpiler/test_optimize_1q_decomposition.py
+++ b/test/python/transpiler/test_optimize_1q_decomposition.py
@@ -745,6 +745,20 @@ def test_nested_control_flow(self):
         result = passmanager.run(test)
         self.assertEqual(result, expected)
 
+    def test_prefer_no_substitution_if_all_ideal(self):
+        """Test that gates are not substituted if all our ideal gates in basis."""
+        target = Target(num_qubits=1)
+        target.add_instruction(HGate(), {(0,): InstructionProperties(error=0)})
+        target.add_instruction(
+            UGate(Parameter("a"), Parameter("b"), Parameter("c")),
+            {(0,): InstructionProperties(error=0)},
+        )
+        qc = QuantumCircuit(1)
+        qc.h(0)
+        opt_pass = Optimize1qGatesDecomposition(target)
+        res = opt_pass(qc)
+        self.assertEqual(res, qc)
+
 
 if __name__ == "__main__":
     unittest.main()