Skip to content

Commit 87ba931

Browse files
committed
Adding transpilation backend/target tests
1 parent 9fc0a9a commit 87ba931

File tree

2 files changed

+290
-4
lines changed

2 files changed

+290
-4
lines changed

azure-quantum/azure/quantum/qiskit/backends/backend.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"crz",
6565
"h",
6666
"s",
67+
"sx",
6768
"sdg",
6869
"swap",
6970
"t",

azure-quantum/tests/unit/test_qiskit.py

Lines changed: 289 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import numpy as np
1212
import collections
1313

14-
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
14+
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile
1515
from qiskit.providers import JobStatus
1616
from qiskit.providers.models import BackendConfiguration
1717
from qiskit.providers import BackendV2 as Backend
@@ -33,9 +33,22 @@
3333
from azure.quantum.qiskit.backends.backend import (
3434
AzureBackend,
3535
AzureQirBackend,
36+
QIR_BASIS_GATES,
3637
)
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
3952
from azure.quantum.target.rigetti import RigettiTarget
4053

4154
# This provider is used to stub out calls to the AzureQuantumProvider
@@ -201,6 +214,130 @@ def _5_qubit_superposition(self):
201214
circuit.measure([0], [0])
202215
return circuit
203216

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+
204341
def _controlled_s(self):
205342
circuit = QuantumCircuit(3)
206343
circuit.t(0)
@@ -502,7 +639,7 @@ def test_qiskit_qir_submit_ionq(self):
502639
self.assertEqual(len(memory), shots)
503640
self.assertTrue(all([shot == "000" or shot == "111" for shot in memory]))
504641
self.assertEqual(counts, result.data()["counts"])
505-
642+
506643
@pytest.mark.ionq
507644
@pytest.mark.live_test
508645
@pytest.mark.xdist_group(name="ionq.simulator")
@@ -804,6 +941,34 @@ def test_ionq_simulator_has_qis_gateset_target(self):
804941
config = backend.configuration()
805942
self.assertEqual(config.gateset, "qis")
806943

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+
807972
@pytest.mark.ionq
808973
def test_ionq_qpu_has_default(self):
809974
provider = DummyProvider()
@@ -1491,6 +1656,72 @@ def test_translate_quantinuum_qir(self):
14911656
self.assertIn("entryPoint", item)
14921657
self.assertIn("arguments", item)
14931658

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+
14941725
@pytest.mark.quantinuum
14951726
@pytest.mark.live_test
14961727
def test_configuration_quantinuum_backends(self):
@@ -1519,6 +1750,34 @@ def test_configuration_quantinuum_backends(self):
15191750
self.assertIsNotNone(target_name)
15201751
self.assertEqual(56, config.num_qubits)
15211752

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+
15221781
@pytest.mark.rigetti
15231782
@pytest.mark.live_test
15241783
@pytest.mark.xdist_group(name=RigettiTarget.QVM.value)
@@ -1672,6 +1931,32 @@ def test_qiskit_get_rigetti_qpu_targets(self):
16721931
self.assertEqual("qir.v1", config.azure["input_data_format"])
16731932
self.assertEqual(MICROSOFT_OUTPUT_DATA_FORMAT_V2, backend._get_output_data_format())
16741933

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+
16751960
@pytest.mark.skip("Skipping tests against QCI's unavailable targets")
16761961
@pytest.mark.qci
16771962
@pytest.mark.live_test

0 commit comments

Comments
 (0)