|
11 | 11 | import numpy as np |
12 | 12 | import collections |
13 | 13 |
|
14 | | -from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister |
| 14 | +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile |
15 | 15 | from qiskit.providers import JobStatus |
16 | 16 | from qiskit.providers.models import BackendConfiguration |
17 | 17 | from qiskit.providers import BackendV2 as Backend |
|
33 | 33 | from azure.quantum.qiskit.backends.backend import ( |
34 | 34 | AzureBackend, |
35 | 35 | AzureQirBackend, |
| 36 | + QIR_BASIS_GATES, |
36 | 37 | ) |
37 | | -from azure.quantum.qiskit.backends.quantinuum import QuantinuumEmulatorQirBackend, QuantinuumQirBackendBase |
38 | | -from azure.quantum.qiskit.backends.ionq import IonQSimulatorQirBackend |
| 38 | +from qiskit.circuit import Instruction |
| 39 | +from qiskit.circuit.library import UGate, U1Gate, U2Gate, U3Gate |
| 40 | + |
| 41 | +from azure.quantum.qiskit.backends.quantinuum import ( |
| 42 | + QuantinuumEmulatorBackend, |
| 43 | + QuantinuumEmulatorQirBackend, |
| 44 | + QuantinuumQirBackendBase, |
| 45 | +) |
| 46 | +from azure.quantum.qiskit.backends.ionq import ( |
| 47 | + IonQSimulatorNativeBackend, |
| 48 | + IonQSimulatorQirBackend, |
| 49 | +) |
| 50 | +from azure.quantum.qiskit.backends.qci import QCISimulatorBackend |
| 51 | +from azure.quantum.qiskit.backends.rigetti import RigettiSimulatorBackend |
39 | 52 | from azure.quantum.target.rigetti import RigettiTarget |
40 | 53 |
|
41 | 54 | # This provider is used to stub out calls to the AzureQuantumProvider |
@@ -201,6 +214,130 @@ def _5_qubit_superposition(self): |
201 | 214 | circuit.measure([0], [0]) |
202 | 215 | return circuit |
203 | 216 |
|
| 217 | + def _assert_transpile_respects_target( |
| 218 | + self, |
| 219 | + backend, |
| 220 | + circuit: QuantumCircuit, |
| 221 | + expected_ops: set[str] | None = None, |
| 222 | + **kwargs, |
| 223 | + ) -> QuantumCircuit: |
| 224 | + """Transpile ``circuit`` for ``backend`` and assert only supported gates remain. |
| 225 | +
|
| 226 | + Parameters |
| 227 | + ---------- |
| 228 | + backend: |
| 229 | + The Azure Quantum backend under test whose ``Target`` defines the |
| 230 | + supported operation set. |
| 231 | + circuit: QuantumCircuit |
| 232 | + The input circuit to transpile. |
| 233 | + expected_ops: set[str] | None |
| 234 | + Optional collection of gate names that must appear in the |
| 235 | + transpiled output. |
| 236 | +
|
| 237 | + Returns |
| 238 | + ------- |
| 239 | + QuantumCircuit |
| 240 | + The transpiled circuit, validated to contain only target-supported |
| 241 | + operations (aside from virtual barriers). |
| 242 | + """ |
| 243 | + transpiled_circuit = transpile(circuit, backend=backend, target=backend.target, **kwargs) |
| 244 | + |
| 245 | + target_ops = {instruction.name for instruction in backend.target.operations} |
| 246 | + transpiled_ops = [instruction.operation.name for instruction in transpiled_circuit.data] |
| 247 | + |
| 248 | + allowed_virtual_ops = {"barrier"} |
| 249 | + unsupported = { |
| 250 | + name |
| 251 | + for name in transpiled_ops |
| 252 | + if name not in target_ops and name not in allowed_virtual_ops |
| 253 | + } |
| 254 | + self.assertFalse( |
| 255 | + unsupported, |
| 256 | + msg=( |
| 257 | + f"Transpiled circuit for backend '{backend.name}' contains unsupported " |
| 258 | + f"operations: {sorted(unsupported)}" |
| 259 | + ), |
| 260 | + ) |
| 261 | + |
| 262 | + if expected_ops: |
| 263 | + missing = set(expected_ops) - set(transpiled_ops) |
| 264 | + self.assertFalse( |
| 265 | + missing, |
| 266 | + msg=( |
| 267 | + f"Transpiled circuit for backend '{backend.name}' is missing expected " |
| 268 | + f"operations: {sorted(missing)}, found: {sorted(transpiled_ops)}" |
| 269 | + ), |
| 270 | + ) |
| 271 | + |
| 272 | + return transpiled_circuit |
| 273 | + |
| 274 | + def _build_non_qir_test_circuit(self) -> Tuple[QuantumCircuit, set[str]]: |
| 275 | + """Create a circuit exercising gates absent from ``QIR_BASIS_GATES``. |
| 276 | +
|
| 277 | + Returns |
| 278 | + ------- |
| 279 | + Tuple[QuantumCircuit, set[str]] |
| 280 | + A two-qubit circuit populated with standard gates not listed in the |
| 281 | + QIR basis, along with the set of those non-QIR gate names. The |
| 282 | + helper fails if no such gates are present, ensuring test coverage |
| 283 | + stays meaningful. |
| 284 | + """ |
| 285 | + circuit = QuantumCircuit(2) |
| 286 | + circuit.append(UGate(np.pi / 3, np.pi / 5, np.pi / 7), [0]) |
| 287 | + circuit.append(U1Gate(np.pi / 9), [0]) |
| 288 | + circuit.append(U2Gate(np.pi / 8, np.pi / 6), [1]) |
| 289 | + circuit.append(U3Gate(np.pi / 4, np.pi / 3, np.pi / 2), [1]) |
| 290 | + circuit.p(np.pi / 7, 0) |
| 291 | + circuit.cp(np.pi / 6, 0, 1) |
| 292 | + circuit.iswap(0, 1) |
| 293 | + circuit.rzx(np.pi / 5, 0, 1) |
| 294 | + circuit.measure_all() |
| 295 | + |
| 296 | + initial_ops = { |
| 297 | + instruction.operation.name |
| 298 | + for instruction in circuit.data |
| 299 | + if instruction.operation.name != "measure" |
| 300 | + } |
| 301 | + non_qir_ops = { |
| 302 | + name |
| 303 | + for name in initial_ops |
| 304 | + if name not in set(QIR_BASIS_GATES) and name != "barrier" |
| 305 | + } |
| 306 | + self.assertTrue( |
| 307 | + non_qir_ops, |
| 308 | + "Non-QIR gates should be present in the test circuit before transpilation.", |
| 309 | + ) |
| 310 | + return circuit, non_qir_ops |
| 311 | + |
| 312 | + def _assert_qir_transpile_decomposes_non_qir_gates(self, backend) -> set[str]: |
| 313 | + """Ensure QIR transpilation removes all non-QIR gates for ``backend``. |
| 314 | +
|
| 315 | + Parameters |
| 316 | + ---------- |
| 317 | + backend: |
| 318 | + The QIR backend whose transpilation behavior is being validated. |
| 319 | +
|
| 320 | + Returns |
| 321 | + ------- |
| 322 | + set[str] |
| 323 | + The set of operation names found in the transpiled circuit, |
| 324 | + guaranteed not to intersect with the generated non-QIR gate set. |
| 325 | + """ |
| 326 | + circuit, non_qir_ops = self._build_non_qir_test_circuit() |
| 327 | + transpiled = self._assert_transpile_respects_target(backend, circuit) |
| 328 | + transpiled_ops = { |
| 329 | + instruction.operation.name for instruction in transpiled.data |
| 330 | + } |
| 331 | + intersection = non_qir_ops & transpiled_ops |
| 332 | + self.assertFalse( |
| 333 | + intersection, |
| 334 | + msg=( |
| 335 | + f"Transpiled circuit for backend '{backend.name}' contains non-QIR gates " |
| 336 | + f"that should have been decomposed: {sorted(intersection)}" |
| 337 | + ), |
| 338 | + ) |
| 339 | + return transpiled_ops |
| 340 | + |
204 | 341 | def _controlled_s(self): |
205 | 342 | circuit = QuantumCircuit(3) |
206 | 343 | circuit.t(0) |
@@ -502,7 +639,7 @@ def test_qiskit_qir_submit_ionq(self): |
502 | 639 | self.assertEqual(len(memory), shots) |
503 | 640 | self.assertTrue(all([shot == "000" or shot == "111" for shot in memory])) |
504 | 641 | self.assertEqual(counts, result.data()["counts"]) |
505 | | - |
| 642 | + |
506 | 643 | @pytest.mark.ionq |
507 | 644 | @pytest.mark.live_test |
508 | 645 | @pytest.mark.xdist_group(name="ionq.simulator") |
@@ -804,6 +941,34 @@ def test_ionq_simulator_has_qis_gateset_target(self): |
804 | 941 | config = backend.configuration() |
805 | 942 | self.assertEqual(config.gateset, "qis") |
806 | 943 |
|
| 944 | + @pytest.mark.ionq |
| 945 | + def test_ionq_transpile_supports_native_instructions(self): |
| 946 | + backend = IonQSimulatorNativeBackend( |
| 947 | + name="ionq.simulator", provider=None, gateset="native" |
| 948 | + ) |
| 949 | + |
| 950 | + circuit = QuantumCircuit(2) |
| 951 | + circuit.append(MSGate(0.1, 0.2), [0, 1]) |
| 952 | + circuit.append(GPIGate(0.3), [0]) |
| 953 | + circuit.append(GPI2Gate(0.4), [1]) |
| 954 | + |
| 955 | + self._assert_transpile_respects_target( |
| 956 | + backend, |
| 957 | + circuit, |
| 958 | + expected_ops={"ms", "gpi", "gpi2"}, |
| 959 | + ) |
| 960 | + |
| 961 | + @pytest.mark.ionq |
| 962 | + def test_ionq_qir_transpile_converts_non_qir_gates(self): |
| 963 | + backend = IonQSimulatorQirBackend(name="ionq.simulator", provider=None) |
| 964 | + |
| 965 | + transpiled_ops = self._assert_qir_transpile_decomposes_non_qir_gates(backend) |
| 966 | + self.assertGreater( |
| 967 | + len(transpiled_ops - {"measure"}), |
| 968 | + 0, |
| 969 | + "Expected decomposed operations besides measurement.", |
| 970 | + ) |
| 971 | + |
807 | 972 | @pytest.mark.ionq |
808 | 973 | def test_ionq_qpu_has_default(self): |
809 | 974 | provider = DummyProvider() |
@@ -1491,6 +1656,72 @@ def test_translate_quantinuum_qir(self): |
1491 | 1656 | self.assertIn("entryPoint", item) |
1492 | 1657 | self.assertIn("arguments", item) |
1493 | 1658 |
|
| 1659 | + @pytest.mark.quantinuum |
| 1660 | + def test_quantinuum_transpile_supports_native_instructions(self): |
| 1661 | + backend = QuantinuumEmulatorBackend( |
| 1662 | + name="quantinuum.sim.h2-1e", provider=None |
| 1663 | + ) |
| 1664 | + |
| 1665 | + circuit = QuantumCircuit(2) |
| 1666 | + circuit.append(Instruction("v", 1, 0, []), [0]) |
| 1667 | + circuit.append(Instruction("vdg", 1, 0, []), [1]) |
| 1668 | + circuit.append(Instruction("zz", 2, 0, [0.5]), [0, 1]) |
| 1669 | + |
| 1670 | + self._assert_transpile_respects_target( |
| 1671 | + backend, |
| 1672 | + circuit, |
| 1673 | + expected_ops={"v", "vdg", "zz"}, |
| 1674 | + ) |
| 1675 | + |
| 1676 | + @pytest.mark.quantinuum |
| 1677 | + def test_quantinuum_qir_transpile_converts_non_qir_gates(self): |
| 1678 | + backend = QuantinuumEmulatorQirBackend(name="quantinuum.sim.h2-1e", provider=None) |
| 1679 | + |
| 1680 | + transpiled_ops = self._assert_qir_transpile_decomposes_non_qir_gates(backend) |
| 1681 | + self.assertGreater( |
| 1682 | + len(transpiled_ops - {"measure"}), |
| 1683 | + 0, |
| 1684 | + "Expected decomposed operations besides measurement.", |
| 1685 | + ) |
| 1686 | + |
| 1687 | + @pytest.mark.quantinuum |
| 1688 | + def test_quantinuum_qir_transpile_decomposes_initialize(self): |
| 1689 | + backend = QuantinuumEmulatorQirBackend(name="quantinuum.sim.h2-1e", provider=None) |
| 1690 | + |
| 1691 | + circuit = QuantumCircuit(1) |
| 1692 | + circuit.initialize([0, 1], 0) |
| 1693 | + |
| 1694 | + # we would get rz, rz, rz, sx, sx, but optimizing should reduce this to just ry |
| 1695 | + transpiled = self._assert_transpile_respects_target( |
| 1696 | + backend, |
| 1697 | + circuit, |
| 1698 | + expected_ops={"reset", "ry"}, |
| 1699 | + optimization_level=2 |
| 1700 | + ) |
| 1701 | + |
| 1702 | + transpiled_ops = [instruction.operation.name for instruction in transpiled.data] |
| 1703 | + |
| 1704 | + self.assertNotIn( |
| 1705 | + "initialize", |
| 1706 | + transpiled_ops, |
| 1707 | + "State preparation should be decomposed for Quantinuum QIR backends.", |
| 1708 | + ) |
| 1709 | + self.assertEqual( |
| 1710 | + transpiled_ops, |
| 1711 | + ["reset", "ry"], |
| 1712 | + f"Unexpected decomposition for Quantinuum QIR transpilation: {transpiled_ops}", |
| 1713 | + ) |
| 1714 | + self.assertEqual( |
| 1715 | + len(transpiled_ops), |
| 1716 | + 2, |
| 1717 | + "Initialize should decompose into exactly two operations for Quantinuum QIR backends.", |
| 1718 | + ) |
| 1719 | + self.assertAlmostEqual( |
| 1720 | + transpiled.data[1].operation.params[0], |
| 1721 | + np.pi, |
| 1722 | + msg="Initialize([0, 1]) should decompose to an ry(pi) rotation.", |
| 1723 | + ) |
| 1724 | + |
1494 | 1725 | @pytest.mark.quantinuum |
1495 | 1726 | @pytest.mark.live_test |
1496 | 1727 | def test_configuration_quantinuum_backends(self): |
@@ -1519,6 +1750,34 @@ def test_configuration_quantinuum_backends(self): |
1519 | 1750 | self.assertIsNotNone(target_name) |
1520 | 1751 | self.assertEqual(56, config.num_qubits) |
1521 | 1752 |
|
| 1753 | + @pytest.mark.rigetti |
| 1754 | + def test_rigetti_qir_transpile_converts_non_qir_gates(self): |
| 1755 | + backend = RigettiSimulatorBackend(name=RigettiTarget.QVM.value, provider=None) |
| 1756 | + |
| 1757 | + transpiled_ops = self._assert_qir_transpile_decomposes_non_qir_gates(backend) |
| 1758 | + self.assertGreater( |
| 1759 | + len(transpiled_ops - {"measure"}), |
| 1760 | + 0, |
| 1761 | + "Expected decomposed operations besides measurement.", |
| 1762 | + ) |
| 1763 | + |
| 1764 | + @pytest.mark.rigetti |
| 1765 | + def test_rigetti_transpile_supports_standard_gates(self): |
| 1766 | + backend = RigettiSimulatorBackend( |
| 1767 | + name=RigettiTarget.QVM.value, provider=None |
| 1768 | + ) |
| 1769 | + |
| 1770 | + circuit = QuantumCircuit(2) |
| 1771 | + circuit.h(0) |
| 1772 | + circuit.cx(0, 1) |
| 1773 | + circuit.measure_all() |
| 1774 | + |
| 1775 | + self._assert_transpile_respects_target( |
| 1776 | + backend, |
| 1777 | + circuit, |
| 1778 | + expected_ops={"h", "cx", "measure"}, |
| 1779 | + ) |
| 1780 | + |
1522 | 1781 | @pytest.mark.rigetti |
1523 | 1782 | @pytest.mark.live_test |
1524 | 1783 | @pytest.mark.xdist_group(name=RigettiTarget.QVM.value) |
@@ -1672,6 +1931,32 @@ def test_qiskit_get_rigetti_qpu_targets(self): |
1672 | 1931 | self.assertEqual("qir.v1", config.azure["input_data_format"]) |
1673 | 1932 | self.assertEqual(MICROSOFT_OUTPUT_DATA_FORMAT_V2, backend._get_output_data_format()) |
1674 | 1933 |
|
| 1934 | + @pytest.mark.qci |
| 1935 | + def test_qci_qir_transpile_converts_non_qir_gates(self): |
| 1936 | + backend = QCISimulatorBackend(name="qci.simulator", provider=None) |
| 1937 | + |
| 1938 | + transpiled_ops = self._assert_qir_transpile_decomposes_non_qir_gates(backend) |
| 1939 | + self.assertGreater( |
| 1940 | + len(transpiled_ops - {"measure"}), |
| 1941 | + 0, |
| 1942 | + "Expected decomposed operations besides measurement.", |
| 1943 | + ) |
| 1944 | + |
| 1945 | + @pytest.mark.qci |
| 1946 | + def test_qci_transpile_supports_barrier(self): |
| 1947 | + backend = QCISimulatorBackend(name="qci.simulator", provider=None) |
| 1948 | + |
| 1949 | + circuit = QuantumCircuit(1) |
| 1950 | + circuit.h(0) |
| 1951 | + circuit.barrier() |
| 1952 | + circuit.measure_all() |
| 1953 | + |
| 1954 | + self._assert_transpile_respects_target( |
| 1955 | + backend, |
| 1956 | + circuit, |
| 1957 | + expected_ops={"h", "barrier", "measure"}, |
| 1958 | + ) |
| 1959 | + |
1675 | 1960 | @pytest.mark.skip("Skipping tests against QCI's unavailable targets") |
1676 | 1961 | @pytest.mark.qci |
1677 | 1962 | @pytest.mark.live_test |
|
0 commit comments