Repository: Qiskit/qiskit — pull request #11995 (python)
Title: Fix use of custom `dt` in `transpile` when target is provided

<!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nFixes #11994, I am not too sure if I should label it as a feature or a bugfix, I think it's both. This PR is currently blocking #11996.\r\n\r\n\r\n### Details and comments\r\n\r\n

Resolved issues:
title: Scheduling stage doesn't use custom dt when target provided
body: ### Environment\r\n\r\n- **Qiskit version**: 1.1.0\r\n- **Python version**: 3.10.0\r\n- **Operating system**: MacOS\r\n\r\n\r\n### What is happening?\r\n\r\nThe expected behavior of `transpile` when a custom `dt` is specified (i.e. sth like `transpile(qc, backend=b, scheduling_method="asap", dt=dt / 2)` is that the scheduling stage will use this custom `dt` instead of the backend's one, but this behavior is not respected if the input backend contains a `target`. I believe that the root of the issue is that `dt` is encoded in the `inst_durations` object that is passed around, but the [`TimeUnitConversion` transformation pass is designed to overwrite `inst_durations` if a target is specified](https://github.com/Qiskit/qiskit/blob/main/qiskit/transpiler/passes/scheduling/time_unit_conversion.py#L43-L46), so the custom dt never gets to be used:\r\n```\r\n# TimeUnitConversion init\r\nself.inst_durations = inst_durations or InstructionDurations()\r\nif target is not None:\r\n     self.inst_durations = target.durations()\r\n```\r\n\r\nIn other words, the priorities are not consistent between the preset passmanager/transpile and the `TimeUnitConversion` pass (one prioritizes the custom durations, the other prioritizes the target).\r\n\r\n### How can we reproduce the issue?\r\n\r\nThis snippet works for the V1 backend and doesn't for the V2 backend:\r\n\r\n```\r\nfrom qiskit import QuantumCircuit\r\nfrom qiskit import transpile\r\nfrom qiskit_ibm_runtime.fake_provider import FakeHanoi, FakeHanoiV2\r\n\r\nbackend_v1 = FakeHanoi()\r\nbackend_v2 = FakeHanoiV2()\r\ndt = 2.2222222222222221e-10\r\n\r\n# from test_change_dt_in_transpile\r\nqc = QuantumCircuit(1, 1)\r\nqc.x(0)\r\nqc.measure(0, 0)\r\n\r\nfor b in [backend_v1, backend_v2]:\r\n  # default case\r\n  scheduled = transpile(qc, backend=b, scheduling_method="asap")\r\n  org_duration = scheduled.duration\r\n\r\n  # halve dt in sec = double duration in dt\r\n  scheduled = transpile(\r\n    qc, backend=b, scheduling_method="asap", dt=dt / 2\r\n  )\r\n  print(b, scheduled.duration == org_duration * 2)\r\n```\r\n\r\n### What should happen?\r\n\r\nThe custom dt shouldn't be bypassed.\r\n\r\n### Any suggestions?\r\n\r\nI think that this issue could be fixed if we establish a consistent priority of the custom `inst_durations` over the target's. A PR with a fix proposal will follow.

Hints:

Fix patch:
diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py\nindex d53c3fc4ef6a..e0931133c52e 100644\n--- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py\n+++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py\n@@ -42,15 +42,16 @@ def __init__(self, inst_durations: InstructionDurations = None, target: Target =\n         Args:\n             inst_durations (InstructionDurations): A dictionary of durations of instructions.\n             target: The :class:`~.Target` representing the target backend, if both\n-                  ``inst_durations`` and ``target`` are specified then this argument will take\n-                  precedence and ``inst_durations`` will be ignored.\n-\n-\n+                  ``inst_durations`` and ``target`` are specified, the ``inst_durations`` argument\n+                  will take precedence and ``target`` will be ignored.\n         """\n         super().__init__()\n-        self.inst_durations = inst_durations or InstructionDurations()\n-        if target is not None:\n-            self.inst_durations = target.durations()\n+        if inst_durations is not None:\n+            self.inst_durations = inst_durations\n+        else:\n+            self.inst_durations = (\n+                target.durations() if target is not None else InstructionDurations()\n+            )\n \n     def run(self, dag: DAGCircuit):\n         """Run the TimeUnitAnalysis pass on `dag`.\ndiff --git a/releasenotes/notes/transpile-custom-dt-precedence-3c7354e40bbd1107.yaml b/releasenotes/notes/transpile-custom-dt-precedence-3c7354e40bbd1107.yaml\nnew file mode 100644\nindex 000000000000..c1436948f953\n--- /dev/null\n+++ b/releasenotes/notes/transpile-custom-dt-precedence-3c7354e40bbd1107.yaml\n@@ -0,0 +1,7 @@\n+---\n+features:\n+  - |\n+    The :class:`.TimeUnitConversion` transformation pass now gives precedence to the ``inst_durations``\n+    argument over the ``target`` argument. This fixes an unexpected behavior in :meth:`.transpile` where\n+    custom ``dt`` inputs would be ignored if a ``target`` was provided too (directly or as part of a \n+    :class:`.BackendV2` instance).\n\\ No newline at end of file\n

Test patch:
diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py\nindex ec4bc72ffd06..9a3ebd5215da 100644\n--- a/test/python/circuit/test_scheduled_circuit.py\n+++ b/test/python/circuit/test_scheduled_circuit.py\n@@ -20,6 +20,7 @@\n from qiskit.circuit import Parameter\n from qiskit.circuit.duration import convert_durations_to_dt\n from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2\n+from qiskit.providers.backend_compat import BackendV2Converter\n from qiskit.providers.basic_provider import BasicSimulator\n from qiskit.scheduler import ScheduleConfig\n from qiskit.transpiler.exceptions import TranspilerError\n@@ -44,6 +45,9 @@ def setUp(self):\n         # run when alignment values different to 1 are found.\n         self.backend_with_dt.configuration().timing_constraints = {}\n         self.backend_without_dt.configuration().timing_constraints = {}\n+        # Generate V2 backends\n+        self.backend_with_dt_v2 = BackendV2Converter(self.backend_with_dt)\n+        self.backend_without_dt_v2 = BackendV2Converter(self.backend_without_dt)\n         self.dt = 2.2222222222222221e-10\n         self.simulator_backend = BasicSimulator()\n \n@@ -79,24 +83,27 @@ def test_schedule_circuit_when_transpile_option_tells_dt(self):\n         qc.delay(100, 0, unit="ns")  # 450[dt]\n         qc.h(0)\n         qc.h(1)\n-        sc = transpile(\n-            qc,\n-            self.backend_without_dt,\n-            scheduling_method="alap",\n-            dt=self.dt,\n-            layout_method="trivial",\n-        )\n-        self.assertEqual(sc.duration, 450546)\n-        self.assertEqual(sc.unit, "dt")\n-        self.assertEqual(sc.data[0].operation.name, "delay")\n-        self.assertEqual(sc.data[0].operation.duration, 450450)\n-        self.assertEqual(sc.data[0].operation.unit, "dt")\n-        self.assertEqual(sc.data[1].operation.name, "rz")\n-        self.assertEqual(sc.data[1].operation.duration, 0)\n-        self.assertEqual(sc.data[1].operation.unit, "dt")\n-        self.assertEqual(sc.data[4].operation.name, "delay")\n-        self.assertEqual(sc.data[4].operation.duration, 450450)\n-        self.assertEqual(sc.data[4].operation.unit, "dt")\n+\n+        for backend in [self.backend_without_dt, self.backend_without_dt_v2]:\n+            with self.subTest(backend=backend):\n+                sc = transpile(\n+                    qc,\n+                    backend,\n+                    scheduling_method="alap",\n+                    dt=self.dt,\n+                    layout_method="trivial",\n+                )\n+                self.assertEqual(sc.duration, 450546)\n+                self.assertEqual(sc.unit, "dt")\n+                self.assertEqual(sc.data[0].operation.name, "delay")\n+                self.assertEqual(sc.data[0].operation.duration, 450450)\n+                self.assertEqual(sc.data[0].operation.unit, "dt")\n+                self.assertEqual(sc.data[1].operation.name, "rz")\n+                self.assertEqual(sc.data[1].operation.duration, 0)\n+                self.assertEqual(sc.data[1].operation.unit, "dt")\n+                self.assertEqual(sc.data[4].operation.name, "delay")\n+                self.assertEqual(sc.data[4].operation.duration, 450450)\n+                self.assertEqual(sc.data[4].operation.unit, "dt")\n \n     def test_schedule_circuit_in_sec_when_no_one_tells_dt(self):\n         """dt is unknown and all delays and gate times are in SI"""\n@@ -245,23 +252,26 @@ def test_unit_seconds_when_using_backend_durations(self):\n         qc.h(0)\n         qc.delay(500 * self.dt, 1, "s")\n         qc.cx(0, 1)\n-        # usual case\n-        scheduled = transpile(\n-            qc, backend=self.backend_with_dt, scheduling_method="alap", layout_method="trivial"\n-        )\n-        self.assertEqual(scheduled.duration, 1876)\n \n-        # update durations\n-        durations = InstructionDurations.from_backend(self.backend_with_dt)\n-        durations.update([("cx", [0, 1], 1000 * self.dt, "s")])\n-        scheduled = transpile(\n-            qc,\n-            backend=self.backend_with_dt,\n-            scheduling_method="alap",\n-            instruction_durations=durations,\n-            layout_method="trivial",\n-        )\n-        self.assertEqual(scheduled.duration, 1500)\n+        for backend in [self.backend_with_dt, self.backend_with_dt_v2]:\n+            with self.subTest(backend=backend):\n+                # usual case\n+                scheduled = transpile(\n+                    qc, backend=backend, scheduling_method="alap", layout_method="trivial"\n+                )\n+                self.assertEqual(scheduled.duration, 1876)\n+\n+                # update durations\n+                durations = InstructionDurations.from_backend(self.backend_with_dt)\n+                durations.update([("cx", [0, 1], 1000 * self.dt, "s")])\n+                scheduled = transpile(\n+                    qc,\n+                    backend=backend,\n+                    scheduling_method="alap",\n+                    instruction_durations=durations,\n+                    layout_method="trivial",\n+                )\n+                self.assertEqual(scheduled.duration, 1500)\n \n     def test_per_qubit_durations(self):\n         """See: https://github.com/Qiskit/qiskit-terra/issues/5109"""\n@@ -348,15 +358,15 @@ def test_change_dt_in_transpile(self):\n         qc = QuantumCircuit(1, 1)\n         qc.x(0)\n         qc.measure(0, 0)\n-        # default case\n-        scheduled = transpile(qc, backend=self.backend_with_dt, scheduling_method="asap")\n-        org_duration = scheduled.duration\n \n-        # halve dt in sec = double duration in dt\n-        scheduled = transpile(\n-            qc, backend=self.backend_with_dt, scheduling_method="asap", dt=self.dt / 2\n-        )\n-        self.assertEqual(scheduled.duration, org_duration * 2)\n+        for backend in [self.backend_with_dt, self.backend_with_dt_v2]:\n+            with self.subTest(backend=backend):\n+                # default case\n+                scheduled = transpile(qc, backend=backend, scheduling_method="asap")\n+                org_duration = scheduled.duration\n+                # halve dt in sec = double duration in dt\n+                scheduled = transpile(qc, backend=backend, scheduling_method="asap", dt=self.dt / 2)\n+                self.assertEqual(scheduled.duration, org_duration * 2)\n \n     @data("asap", "alap")\n     def test_duration_on_same_instruction_instance(self, scheduling_method):\n