diff --git a/crates/cext/src/transpiler/transpile_layout.rs b/crates/cext/src/transpiler/transpile_layout.rs index 943b699a1b51..adf188d8b03d 100644 --- a/crates/cext/src/transpiler/transpile_layout.rs +++ b/crates/cext/src/transpiler/transpile_layout.rs @@ -13,6 +13,13 @@ use crate::pointers::const_ptr_as_ref; use qiskit_transpiler::transpile_layout::TranspileLayout; +#[cfg(feature = "python_binding")] +use pyo3::Python; +#[cfg(feature = "python_binding")] +use pyo3::ffi::PyObject; +#[cfg(feature = "python_binding")] +use qiskit_circuit::circuit_data::CircuitData; + /// @ingroup QkTranspileLayout /// Return the number of qubits in the input circuit to the transpiler. /// @@ -239,6 +246,41 @@ pub unsafe extern "C" fn qk_transpile_layout_free(layout: *mut TranspileLayout) } } +/// @ingroup QkTranspileLayout +/// Generate a Python-space ``TranspileLayout`` object from a ``QkTranspileLayout``. +/// +/// The created Python-space object is a copy of the ``QkTranspileLayout`` provided, the data +/// representation is different between C and Python and the data is not moved to Python like +/// for some other ``*_to_python`` functions. +/// +/// @param layout a pointer to a ``QkTranspileLayout``. +/// @param circuit a pointer to the original ``QkCircuit``. +/// @return the PyObject pointer for the Python space TranspileLayout object. +/// +/// # Safety +/// +/// Behavior is undefined if ``layout`` and ``circuit`` are not valid, non-null pointers to a +/// ``QkTranspileLayout`` and ``QkCircuit`` respectively. It is assumed that the thread currently +/// executing this function holds the Python GIL. This is required to create the Python object +/// returned by this function. +#[unsafe(no_mangle)] +#[cfg(feature = "python_binding")] +#[cfg(feature = "cbinding")] +pub unsafe extern "C" fn qk_transpile_layout_to_python( + layout: *const TranspileLayout, + circuit: *const CircuitData, +) -> *mut PyObject { + // SAFETY: Per the documentation layout and circuit are valid pointers + // and the thread running the function holds the gil. + unsafe { + let layout = const_ptr_as_ref(layout); + let circuit = const_ptr_as_ref(circuit); + let py = Python::assume_attached(); + let res = layout.to_py_native(py, circuit.qubits().objects()).unwrap(); + res.into_ptr() + } +} + #[cfg(test)] mod test { use super::*; diff --git a/test/python/c_api/__init__.py b/test/python/c_api/__init__.py new file mode 100644 index 000000000000..bacd10889f9f --- /dev/null +++ b/test/python/c_api/__init__.py @@ -0,0 +1,13 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Qiskit unit tests of C API.""" diff --git a/test/python/c_api/ffi.py b/test/python/c_api/ffi.py new file mode 100644 index 000000000000..bf734215639d --- /dev/null +++ b/test/python/c_api/ffi.py @@ -0,0 +1,265 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""FFI to call qiskit C API.""" + +import ctypes + +import numpy + +import qiskit + +LIB_PATH = qiskit._accelerate.__file__ +LIB = ctypes.PyDLL(LIB_PATH) + + +class QkCircuit(ctypes.Structure): + """QkCircuit Opaque Type""" + + pass + + +class QkTarget(ctypes.Structure): + """QkTarget Opaque Type""" + + pass + + +class QkTargetEntry(ctypes.Structure): + """QkTargetEntry Opaque Type""" + + pass + + +class QkTranspileLayout(ctypes.Structure): + """QkTranspileLayout Opaque Type""" + + pass + + +class QkTranspileOptions(ctypes.Structure): + """QkTranspileOptions struct""" + + _fields_ = [ + ("optimization_level", ctypes.c_uint8), + ("seed", ctypes.c_int64), + ("approximation_degree", ctypes.c_double), + ] + + +class QkTranspileResult(ctypes.Structure): + """QkTranspileResult struct""" + + _fields_ = [ + ("circuit", ctypes.POINTER(QkCircuit)), + ("layout", ctypes.POINTER(QkTranspileLayout)), + ] + + +LIB.qk_circuit_new.argtypes = [ctypes.c_uint32, ctypes.c_uint32] +LIB.qk_circuit_new.restype = ctypes.POINTER(QkCircuit) +LIB.qk_circuit_free.argtypes = [ctypes.POINTER(QkCircuit)] +LIB.qk_circuit_gate.argtypes = [ + ctypes.POINTER(QkCircuit), + ctypes.c_uint8, + ctypes.POINTER(ctypes.c_uint32), + ctypes.POINTER(ctypes.c_double), +] +LIB.qk_circuit_gate.restype = ctypes.c_uint32 +LIB.qk_circuit_measure.argtypes = [ctypes.POINTER(QkCircuit), ctypes.c_uint32, ctypes.c_uint32] +LIB.qk_circuit_to_python.argtypes = [ + ctypes.POINTER(QkCircuit), +] +LIB.qk_circuit_to_python.restype = ctypes.py_object +LIB.qk_circuit_barrier.argtypes = [ + ctypes.POINTER(QkCircuit), + ctypes.POINTER(ctypes.c_uint32), + ctypes.c_uint32, +] +LIB.qk_circuit_barrier.restype = ctypes.c_uint32 + +LIB.qk_target_new.argtypes = [ + ctypes.c_uint32, +] +LIB.qk_target_new.restype = ctypes.POINTER(QkTarget) +LIB.qk_target_free.argtypes = [ctypes.POINTER(QkTarget)] +LIB.qk_target_entry_new.argtypes = [ + ctypes.c_uint8, +] +LIB.qk_target_entry_new.restype = ctypes.POINTER(QkTargetEntry) +LIB.qk_target_entry_new_measure.argtypes = [] +LIB.qk_target_entry_new_measure.restype = ctypes.POINTER(QkTargetEntry) +LIB.qk_target_entry_add_property.argtypes = [ + ctypes.POINTER(QkTargetEntry), + ctypes.POINTER(ctypes.c_uint32), + ctypes.c_uint32, + ctypes.c_double, + ctypes.c_double, +] +LIB.qk_target_entry_add_property.restype = ctypes.c_uint32 +LIB.qk_target_add_instruction.argtypes = [ctypes.POINTER(QkTarget), ctypes.POINTER(QkTargetEntry)] +LIB.qk_target_add_instruction.restype = ctypes.c_uint32 +LIB.qk_transpile.argtypes = [ + ctypes.POINTER(QkCircuit), + ctypes.POINTER(QkTarget), + ctypes.POINTER(QkTranspileOptions), + ctypes.POINTER(QkTranspileResult), + ctypes.POINTER(ctypes.c_char_p), +] +LIB.qk_transpile.restype = ctypes.c_uint32 +LIB.qk_transpile_layout_to_python.argtypes = [ + ctypes.POINTER(QkTranspileLayout), + ctypes.POINTER(QkCircuit), +] +LIB.qk_transpile_layout_to_python.restype = ctypes.py_object +LIB.qk_transpile_layout_free.argtypes = [ctypes.POINTER(QkTranspileLayout)] + + +def into_c_array_ptr(lst, ctype): + """Convert a list into a c array pointer that we can pass to the C API.""" + c_array = (ctype * len(lst))(*lst) + return ctypes.cast(c_array, ctypes.POINTER(ctype)) + + +def build_circuit_from_python(circuit: qiskit.circuit.QuantumCircuit) -> ctypes.POINTER(QkCircuit): + """Convert a Python circuit to a C circuit if compatible.""" + + c_qc = LIB.qk_circuit_new(circuit.num_qubits, circuit.num_clbits) + for inst in circuit.data: + qubit_idx = [circuit.find_bit(x).index for x in inst.qubits] + qubits = into_c_array_ptr(qubit_idx, ctypes.c_uint32) + params = into_c_array_ptr(inst.params, ctypes.c_double) + if isinstance(inst.operation, qiskit.circuit.Measure): + clbit = circuit.find_bit(inst.clbits[0]).index + LIB.qk_circuit_measure(c_qc, qubit_idx[0], clbit) + elif isinstance(inst.operation, qiskit.circuit.Barrier): + LIB.qk_circuit_barrier(c_qc, qubits, len(qubit_idx)) + else: + LIB.qk_circuit_gate( + c_qc, + int(inst.operation._standard_gate), + qubits, + params, + ) + return c_qc + + +def build_homogenous_target( + cmap: qiskit.transpiler.CouplingMap, + basis_gates: list[str], + seed: int, + ideal_gates: bool = False, +) -> QkTarget: + """Build a C API QkTarget with a homogenous gate set. + + This function will build a target taking a coupling map and basis gate + list. It assumes that all 1q gates are on all qubits and all 2q gates are on + all coupling map edges. Larger gates (and 0 qubit gates) are not supported + and will error. All gates will have a random error rate and duration assigned on + each qubit. Measurements will be added as a supported instruction on + every qubit unconditionally. + + Args: + cmap: The coupling map representing the connectivity of the target to generate + basis_gates: The list of standard gate name strings to use in the target + seed: An rng seed + ideal_gates: If set to False no error rate or duration will be assigned to + any instruction on the target. + + Returns: + A ctypes pointer to the C API QkTarget object + """ + + name_mapping = qiskit.circuit.library.standard_gates.get_standard_gate_name_mapping() + c_target = LIB.qk_target_new(cmap.size()) + rng = numpy.random.default_rng(seed) + for gate in basis_gates: + gate_obj = name_mapping[gate] + entry = LIB.qk_target_entry_new(int(gate_obj._standard_gate)) + if gate_obj.num_qubits == 2: + for edge in cmap.get_edges(): + qubits = into_c_array_ptr(edge, ctypes.c_uint32) + if not ideal_gates: + error = rng.uniform(0.0, 1.0) + duration = rng.uniform(0.0, 1.0) + else: + error = float("nan") + duration = float("nan") + LIB.qk_target_entry_add_property(entry, qubits, 2, duration, error) + elif gate_obj.num_qubits == 1: + for qubit in range(cmap.size()): + qubits = into_c_array_ptr([qubit], ctypes.c_uint32) + if not ideal_gates: + error = rng.uniform(0.0, 1.0) + duration = rng.uniform(0.0, 1.0) + else: + error = float("nan") + duration = float("nan") + LIB.qk_target_entry_add_property(entry, qubits, 1, duration, error) + else: + raise qiskit.transpiler.exceptions.TranspilerError(f"Invalid gate: {gate}") + LIB.qk_target_add_instruction(c_target, entry) + measure_entry = LIB.qk_target_entry_new_measure() + for qubit in range(cmap.size()): + qubits = into_c_array_ptr([qubit], ctypes.c_uint32) + if not ideal_gates: + error = rng.uniform(0.0, 1.0) + duration = rng.uniform(0.0, 1.0) + else: + error = float("nan") + duration = float("nan") + + LIB.qk_target_entry_add_property(measure_entry, qubits, 1, duration, error) + LIB.qk_target_add_instruction(c_target, measure_entry) + return c_target + + +def transpile_from_c( + circuit: ctypes.POINTER(QkCircuit), + target: ctypes.POINTER(QkTarget), + optimization_level, + approximation_degree, + seed, +) -> qiskit.QuantumCircuit: + """Transpile a circuit and return it as a python circuit.""" + args = [] + if optimization_level is not None: + args.append(optimization_level) + else: + args.append(2) + if seed is not None: + args.append(seed) + else: + args.append(-1) + if approximation_degree is not None: + args.append(approximation_degree) + else: + args.append(1.0) + options = QkTranspileOptions(*args) + result = QkTranspileResult(ctypes.POINTER(QkCircuit)(), ctypes.POINTER(QkTranspileLayout)()) + error = ctypes.pointer(ctypes.c_char_p(None)) + res = LIB.qk_transpile( + circuit, + target, + ctypes.pointer(options), + ctypes.pointer(result), + error, + ) + if res != 0: + raise qiskit.transpiler.exceptions.TranspilerError( + f"Transpilation failed: {error.contents.value.decode('utf8')}" + ) + layout = LIB.qk_transpile_layout_to_python(result.layout, result.circuit) + LIB.qk_transpile_layout_free(result.layout) + out = LIB.qk_circuit_to_python(result.circuit) + out._layout = layout + return out diff --git a/test/python/c_api/test_transpile.py b/test/python/c_api/test_transpile.py new file mode 100644 index 000000000000..a6b44ce6b55a --- /dev/null +++ b/test/python/c_api/test_transpile.py @@ -0,0 +1,327 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring + +import math + +from test import QiskitTestCase, combine + +import ddt +import rustworkx as rx + +from qiskit.circuit import QuantumCircuit, Clbit, QuantumRegister, ClassicalRegister +from qiskit.circuit.library import CXGate +from qiskit.transpiler import CouplingMap +from qiskit.quantum_info import Operator + +from . import ffi +from ..legacy_cmaps import MELBOURNE_CMAP + + +@ddt.ddt +class TestTranspile(QiskitTestCase): + @ddt.data(0, 1, 2, 3) + def test_empty_transpilation(self, opt_level): + """Test transpilation of empty circuit.""" + target = ffi.build_homogenous_target(CouplingMap.from_ring(10), ["cx", "u"], 42) + self.addCleanup(ffi.LIB.qk_target_free, target) + circuit = QuantumCircuit(5, 5) + c_qc = ffi.build_circuit_from_python(circuit) + self.addCleanup(ffi.LIB.qk_circuit_free, c_qc) + res = ffi.transpile_from_c(c_qc, target, opt_level, 1.0, 42) + # Remove layout since it's not valid for this comparison it just says a layout of empty + # qubits was selected with no permutation + res._layout = None + expected = QuantumCircuit(10) + expected.add_bits([Clbit() for _ in range(5)]) + self.assertEqual(expected, res) + + @ddt.data(0, 1, 2, 3) + def test_transpile_qft_grid(self, opt_level): + """Transpile pipeline can handle 8-qubit QFT on 14-qubit grid.""" + + basis_gates = ["cx", "id", "rz", "sx", "x"] + + qr = QuantumRegister(8) + circuit = QuantumCircuit(qr) + for i, q in enumerate(qr): + for j in range(i): + circuit.cp(math.pi / float(2 ** (i - j)), q, qr[j]) + circuit.h(q) + c_qc = ffi.build_circuit_from_python(circuit) + self.addCleanup(ffi.LIB.qk_circuit_free, c_qc) + target = ffi.build_homogenous_target(CouplingMap(MELBOURNE_CMAP), basis_gates, seed=42) + self.addCleanup(ffi.LIB.qk_target_free, target) + new_circuit = ffi.transpile_from_c(c_qc, target, opt_level, 1.0, 42) + + qubit_indices = {bit: idx for idx, bit in enumerate(new_circuit.qubits)} + for instruction in new_circuit.data: + if isinstance(instruction.operation, CXGate): + self.assertIn([qubit_indices[x] for x in instruction.qubits], MELBOURNE_CMAP) + + @ddt.data(0, 1, 2, 3) + def test_translate_ecr_basis(self, optimization_level): + """Verify that rewriting in ECR basis is efficient.""" + circuit = QuantumCircuit(2) + circuit.rzx(0.121234, 0, 1) + circuit.cx(0, 1) + circuit.swap(0, 1) + circuit.iswap(0, 1) + c_qc = ffi.build_circuit_from_python(circuit) + self.addCleanup(ffi.LIB.qk_circuit_free, c_qc) + target = ffi.build_homogenous_target(CouplingMap.from_full(2), ["u", "ecr"], 42) + self.addCleanup(ffi.LIB.qk_target_free, target) + res = ffi.transpile_from_c(c_qc, target, optimization_level, 1.0, 42) + # Swap gates get optimized away in opt. level 2, 3 + expected_num_ecr_gates = 2 if optimization_level in (2, 3) else 8 + self.assertEqual(res.count_ops()["ecr"], expected_num_ecr_gates) + self.assertEqual(Operator(circuit), Operator.from_circuit(res)) + + def test_optimize_ecr_basis(self): + """Test highest optimization level can optimize over ECR.""" + circuit = QuantumCircuit(2) + circuit.swap(1, 0) + circuit.iswap(0, 1) + c_qc = ffi.build_circuit_from_python(circuit) + self.addCleanup(ffi.LIB.qk_circuit_free, c_qc) + target = ffi.build_homogenous_target(CouplingMap.from_full(2), ["u", "ecr"], 42) + self.addCleanup(ffi.LIB.qk_target_free, target) + res = ffi.transpile_from_c(c_qc, target, 3, 1.0, 42) + # an iswap gate is equivalent to (swap, CZ) up to single-qubit rotations. Normally, the swap gate + # in the circuit would cancel with the swap gate of the (swap, CZ), leaving a single CZ gate that + # can be realized via one ECR gate. However, with the introduction of ElideSwap, the swap gate + # cancellation can not occur anymore, thus requiring two ECR gates for the iswap gate. + self.assertEqual(res.count_ops()["ecr"], 2) + self.assertEqual(Operator(circuit), Operator.from_circuit(res)) + + @ddt.data(0, 1, 2, 3) + def test_target_ideal_gates(self, opt_level): + """Test that transpile() with a custom ideal sim target works.""" + qubit_reg = QuantumRegister(2, name="q") + clbit_reg = ClassicalRegister(2, name="c") + qc = QuantumCircuit(qubit_reg, clbit_reg, name="bell") + qc.h(qubit_reg[0]) + qc.cx(qubit_reg[0], qubit_reg[1]) + c_qc = ffi.build_circuit_from_python(qc) + target = ffi.build_homogenous_target( + CouplingMap.from_full(2), ["u", "cx"], 42, ideal_gates=True + ) + result = ffi.transpile_from_c(c_qc, target, opt_level, 1.0, 42) + + self.assertEqual(Operator.from_circuit(result), Operator.from_circuit(qc)) + + @combine(opt_level=[0, 1, 2, 3], basis=[["rz", "x"], ["rx", "z"], ["rz", "y"], ["ry", "x"]]) + def test_paulis_to_constrained_1q_basis(self, opt_level, basis): + """Test that Pauli-gate circuits can be transpiled to constrained 1q bases that do not + contain any root-Pauli gates.""" + qc = QuantumCircuit(1) + qc.x(0) + qc.barrier() + qc.y(0) + qc.barrier() + qc.z(0) + target = ffi.build_homogenous_target(CouplingMap.from_line(1), basis, 42, True) + self.addCleanup(ffi.LIB.qk_target_free, target) + c_qc = ffi.build_circuit_from_python(qc) + self.addCleanup(ffi.LIB.qk_circuit_free, c_qc) + transpiled = ffi.transpile_from_c(c_qc, target, opt_level, 1.0, 42) + self.assertGreaterEqual(set(basis) | {"barrier"}, transpiled.count_ops().keys()) + self.assertEqual(Operator(qc), Operator(transpiled)) + + @ddt.data(0, 1, 2, 3) + def test_single_qubit_circuit_deterministic_output(self, optimization_level): + """Test that the transpiler's output is deterministic in a single qubit example. + + Reproduce from `#14729 `__""" + params = [math.pi, math.pi / 2, math.pi * 2, math.pi / 4, 0] + + circ = QuantumCircuit(len(params)) + for i, par in enumerate(params): + circ.rx(par, i) + circ.measure_all() + target = ffi.build_homogenous_target( + CouplingMap.from_full(10), ["cx", "rz", "sx", "x"], 123 + ) + self.addCleanup(ffi.LIB.qk_target_free, target) + c_circ = ffi.build_circuit_from_python(circ) + self.addCleanup(ffi.LIB.qk_circuit_free, c_circ) + isa_circs = [] + for _ in range(10): + isa_circs.append(ffi.transpile_from_c(c_circ, target, optimization_level, 1.0, 123)) + for i in range(10): + self.assertEqual(isa_circs[0], isa_circs[i]) + + @ddt.data(2, 3) + def test_size_optimization(self, level): + """Test the levels for optimization based on size of circuit""" + target = ffi.build_homogenous_target(CouplingMap.from_full(8), ["u3", "cx"], 42, True) + self.addCleanup(ffi.LIB.qk_target_free, target) + qc = QuantumCircuit(8) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(5, 4) + qc.cx(6, 5) + qc.cx(4, 5) + qc.cx(3, 4) + qc.cx(5, 6) + qc.cx(5, 4) + qc.cx(3, 4) + qc.cx(2, 3) + qc.cx(1, 2) + qc.cx(6, 7) + qc.cx(6, 5) + qc.cx(5, 4) + qc.cx(7, 6) + qc.cx(6, 7) + c_circ = ffi.build_circuit_from_python(qc) + self.addCleanup(ffi.LIB.qk_circuit_free, c_circ) + circ = ffi.transpile_from_c(c_circ, target, level, 1.0, 123) + + circ_data = circ.data + free_qubits = {0, 1, 2, 3} + + # ensure no gates are using qubits - [0,1,2,3] + for gate in circ_data: + layout = circ.layout.initial_layout + indices = {layout[circ.find_bit(qubit).index] for qubit in gate.qubits} + common = indices.intersection(free_qubits) + for common_qubit in common: + self.assertTrue(common_qubit not in free_qubits) + + self.assertLess(circ.size(), qc.size()) + self.assertLessEqual(circ.depth(), qc.depth()) + + @ddt.data(0, 1, 2, 3) + def test_single_cx_gate_circuit_on_linear_backend(self, level): + """Simple coupling map (linear 5 qubits).""" + basis = ["u1", "u2", "cx", "swap"] + coupling_map = CouplingMap([(0, 1), (1, 2), (2, 3), (3, 4)]) + circuit = QuantumCircuit(5) + circuit.cx(2, 4) + c_circ = ffi.build_circuit_from_python(circuit) + self.addCleanup(ffi.LIB.qk_circuit_free, c_circ) + + target = ffi.build_homogenous_target(coupling_map, basis, seed=24) + self.addCleanup(ffi.LIB.qk_target_free, target) + result = ffi.transpile_from_c(c_circ, target, level, 1.0, 123) + + self.assertIsInstance(result, QuantumCircuit) + self.assertEqual(Operator.from_circuit(result), Operator(circuit)) + + @ddt.data(0, 1, 2, 3) + def test_multiple_cx_gate_circuit_on_linear_backend(self, level): + """Simple coupling map (linear 5 qubits).""" + basis = ["u1", "u2", "cx", "swap"] + circuit = QuantumCircuit(5) + circuit.cx(0, 4) + circuit.cx(1, 4) + circuit.cx(2, 4) + circuit.cx(3, 4) + coupling_map = CouplingMap([(0, 1), (1, 2), (2, 3), (3, 4)]) + c_circ = ffi.build_circuit_from_python(circuit) + self.addCleanup(ffi.LIB.qk_circuit_free, c_circ) + target = ffi.build_homogenous_target(coupling_map, basis, seed=24) + self.addCleanup(ffi.LIB.qk_target_free, target) + result = ffi.transpile_from_c(c_circ, target, level, 1.0, 123) + self.assertIsInstance(result, QuantumCircuit) + self.assertEqual(Operator.from_circuit(result), Operator(circuit)) + + +@ddt.ddt +class TestTranspileMultiChipTarget(QiskitTestCase): + """Test qk_transpile() with a disjoint coupling map.""" + + def setUp(self): + super().setUp() + + graph = rx.generators.directed_heavy_hex_graph(3) + edges = [] + for i in range(3): + for root_edge in graph.edge_list(): + offset = i * len(graph) + edge = (root_edge[0] + offset, root_edge[1] + offset) + edges.append(edge) + self.edge_set = set(edges) + cmap = CouplingMap(edges) + self.target = ffi.build_homogenous_target(cmap, ["rz", "x", "sx", "cz"], seed=12345678942) + self.addCleanup(ffi.LIB.qk_target_free, self.target) + + @ddt.data(0, 1, 2, 3) + def test_basic_connected_circuit(self, opt_level): + """Test basic connected circuit on disjoint backend""" + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.measure_all() + c_circ = ffi.build_circuit_from_python(qc) + self.addCleanup(ffi.LIB.qk_circuit_free, c_circ) + tqc = ffi.transpile_from_c(c_circ, self.target, opt_level, 1.0, 123) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + if len(qubits) == 2: + self.assertIn(qubits, self.edge_set) + + @ddt.data(0, 1, 2, 3) + def test_triple_circuit(self, opt_level): + """Test a split circuit with one circuit component per chip.""" + qc = QuantumCircuit(30) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.measure_all() + c_circ = ffi.build_circuit_from_python(qc) + self.addCleanup(ffi.LIB.qk_circuit_free, c_circ) + + if opt_level == 0: + self.skipTest("Invalid layout for this backend causes a panic in sabre") + tqc = ffi.transpile_from_c(c_circ, self.target, opt_level, 1.0, 123) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + if len(qubits) == 2: + self.assertIn(qubits, self.edge_set)