Repository: Qiskit/qiskit

Issue:
Round durations in `GenericBackendV2`
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary
This PR makes sure that the conversion of `GenericBackendV2` instruction durations to `dt` is exact to avoid user warnings during scheduling of type:

`UserWarning: Duration is rounded to 616 [dt] = 1.367520e-07 [s] from 1.366887e-07 [s]`

Given that the durations are sampled randomly, and the rounded duration is the one used in the scheduling passes, we might as well make sure in advance that the conversion from seconds to dt will be exact and doesn't raise warnings.

### Details and comments
I am not sure this qualifies as a bugfix but I think it improves the readability of the test logs. For example, for `test_scheduling_backend_v2` in `test/python/compiler/test_transpiler.py`. Before:

```
/Users/ept/qiskit_workspace/qiskit/qiskit/circuit/duration.py:37: UserWarning: Duration is rounded to 986 [dt] = 2.188920e-07 [s] from 2.189841e-07 [s]
  warnings.warn(
/Users/ept/qiskit_workspace/qiskit/qiskit/circuit/duration.py:37: UserWarning: Duration is rounded to 2740 [dt] = 6.082800e-07 [s] from 6.083383e-07 [s]
  warnings.warn(
/Users/ept/qiskit_workspace/qiskit/qiskit/circuit/duration.py:37: UserWarning: Duration is rounded to 2697 [dt] = 5.987340e-07 [s] from 5.988312e-07 [s]
  warnings.warn(
/Users/ept/qiskit_workspace/qiskit/qiskit/circuit/duration.py:37: UserWarning: Duration is rounded to 178 [dt] = 3.951600e-08 [s] from 3.956636e-08 [s]
  warnings.warn(
.
----------------------------------------------------------------------
Ran 1 test in 0.548s

OK
```

After:

```
.
----------------------------------------------------------------------
Ran 1 test in 0.506s

OK
```



Discussion:

One or more of the the following people are requested to review this:
- `@Qiskit/terra-core`


Resolving patch:
diff --git a/qiskit/circuit/duration.py b/qiskit/circuit/duration.py
--- a/qiskit/circuit/duration.py
+++ b/qiskit/circuit/duration.py
@@ -78,8 +78,15 @@ def convert_durations_to_dt(qc: QuantumCircuit, dt_in_sec: float, inplace=True):
         operation.duration = duration_in_dt(duration, dt_in_sec)
         operation.unit = "dt"
 
-    if circ.duration is not None:
-        circ.duration = duration_in_dt(circ.duration, dt_in_sec)
+    if circ.duration is not None and circ.unit != "dt":
+        if not circ.unit.endswith("s"):
+            raise CircuitError(f"Invalid time unit: '{circ.unit}'")
+
+        duration = circ.duration
+        if circ.unit != "s":
+            duration = apply_prefix(duration, circ.unit)
+
+        circ.duration = duration_in_dt(duration, dt_in_sec)
         circ.unit = "dt"
 
     if not inplace:


Test patch:
diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py
--- a/test/python/circuit/test_scheduled_circuit.py
+++ b/test/python/circuit/test_scheduled_circuit.py
@@ -18,8 +18,10 @@
 from qiskit import QuantumCircuit, QiskitError
 from qiskit import transpile, assemble
 from qiskit.circuit import Parameter
-from qiskit.providers.fake_provider import Fake27QPulseV1
+from qiskit.circuit.duration import convert_durations_to_dt
+from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2
 from qiskit.providers.basic_provider import BasicSimulator
+from qiskit.scheduler import ScheduleConfig
 from qiskit.transpiler.exceptions import TranspilerError
 from qiskit.transpiler.instruction_durations import InstructionDurations
 from test import QiskitTestCase  # pylint: disable=wrong-import-order
@@ -300,6 +302,48 @@ def test_per_qubit_durations(self):
         self.assertEqual(sc.qubit_start_time(*q), 300)
         self.assertEqual(sc.qubit_stop_time(*q), 2400)
 
+    def test_convert_duration_to_dt(self):
+        """Test that circuit duration unit conversion is applied only when necessary.
+        Tests fix for bug reported in PR #11782."""
+
+        backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=10)
+        schedule_config = ScheduleConfig(
+            inst_map=backend.target.instruction_schedule_map(),
+            meas_map=backend.meas_map,
+            dt=backend.dt,
+        )
+
+        circ = QuantumCircuit(2)
+        circ.cx(0, 1)
+        circ.measure_all()
+
+        circuit_dt = transpile(circ, backend, scheduling_method="asap")
+        # reference duration and unit in dt
+        ref_duration = circuit_dt.duration
+        ref_unit = circuit_dt.unit
+
+        circuit_s = circuit_dt.copy()
+        circuit_s.duration *= backend.dt
+        circuit_s.unit = "s"
+
+        circuit_ms = circuit_s.copy()
+        circuit_ms.duration *= 1000
+        circuit_ms.unit = "ms"
+
+        for circuit in [circuit_dt, circuit_s, circuit_ms]:
+            with self.subTest(circuit=circuit):
+                converted_circ = convert_durations_to_dt(
+                    circuit, dt_in_sec=schedule_config.dt, inplace=False
+                )
+                self.assertEqual(
+                    converted_circ.duration,
+                    ref_duration,
+                )
+                self.assertEqual(
+                    converted_circ.unit,
+                    ref_unit,
+                )
+
     def test_change_dt_in_transpile(self):
         qc = QuantumCircuit(1, 1)
         qc.x(0)