From 8928126f8430d9da834185bc63d463810b434ca4 Mon Sep 17 00:00:00 2001 From: Abdullah Ash Saki Date: Fri, 15 Aug 2025 15:29:23 -0400 Subject: [PATCH 1/5] lucj preset pass manager --- .../ffsim/qiskit/transpiler_passes/lucj_pm.py | 380 ++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 python/ffsim/qiskit/transpiler_passes/lucj_pm.py diff --git a/python/ffsim/qiskit/transpiler_passes/lucj_pm.py b/python/ffsim/qiskit/transpiler_passes/lucj_pm.py new file mode 100644 index 000000000..9bfcedd0f --- /dev/null +++ b/python/ffsim/qiskit/transpiler_passes/lucj_pm.py @@ -0,0 +1,380 @@ +import warnings +from qiskit.providers import BackendV2 +from typing import Literal, Sequence, Any +from qiskit import QuantumCircuit +from qiskit.transpiler import generate_preset_pass_manager, StagedPassManager +from qiskit.transpiler.passes import VF2PostLayout, ApplyLayout +from qiskit.passmanager.flow_controllers import ConditionalController +from qiskit.transpiler import PassManager + +import copy +from typing import Sequence + +from ffsim.qiskit import PRE_INIT + +import rustworkx +from qiskit.providers import BackendV2 +from qiskit_ibm_runtime.fake_provider import FakeSherbrooke, FakeTorino +from rustworkx import NoEdgeBetweenNodes, PyGraph + + +"""Funtionality + +1. Throw warning if qiskit transpiler's intial_layout and layout_method are specified. + These two directly conflicts with LUCJ's need for custom layout +2. +""" + +def _create_two_linear_chains(num_orbitals: int) -> PyGraph: + """In zig-zag layout, there are two linear chains (with connecting qubits between + the chains). This function creates those two linear chains: a rustworkx PyGraph + with two disconnected linear chains. Each chain contains `num_orbitals` number + of nodes, i.e., in the final graph there are `2 * num_orbitals` number of nodes. + + Args: + num_orbitals (int): Number orbitals or nodes in each linear chain. They are + also known as alpha-alpha interaction qubits. + + Returns: + A rustworkx.PyGraph with two disconnected linear chains each with `num_orbitals` + number of nodes. + """ + G = rustworkx.PyGraph() + + for n in range(num_orbitals): + G.add_node(n) + + for n in range(num_orbitals - 1): + G.add_edge(n, n + 1, None) + + for n in range(num_orbitals, 2 * num_orbitals): + G.add_node(n) + + for n in range(num_orbitals, 2 * num_orbitals - 1): + G.add_edge(n, n + 1, None) + + return G + +def get_layout_graph_and_allowed_alpha_beta_indices( + num_orbitals: int, + backend_coupling_graph: PyGraph, + topology: str, + alpha_beta_indices: list[tuple[int, int]], +) -> tuple[PyGraph, list[tuple[int, int]]]: + """This function creates the complete zigzag graph that 'can be mapped' to a IBM QPU with + heavy-hex connectivity (the zigzag must be an isomorphic sub-graph to the QPU/backend + coupling graph for it to be mapped). + The zigzag pattern includes both linear chains (alpha-alpha interactions) and connecting + qubits between the linear chains (alpha-beta interactions). + + Args: + num_orbitals (int): Number of orbitals, i.e., number of nodes in each alpha-alpha linear chain. + backend_coupling_graph (PyGraph): The coupling graph of the backend on which the LUCJ ansatz + will be mapped and run. This function takes the coupling graph as a undirected + `rustworkx.PyGraph` where there is only one 'undirected' edge between two nodes, + i.e., qubits. Usually, the coupling graph of a IBM backend is directed (e.g., Eagle devices + such as ibm_sherbrooke) or may have two edges between two nodes (e.g., Heron `ibm_torino`). + A user needs to be make such graphs undirected and/or remove duplicate edges to make them + compatible with this function. One way to do this is as follows: + ``` + graph = backend.coupling_map.graph + if not graph.is_symmetric(): + graph.make_symmetric() + backend_coupling_graph = graph.to_undirected() + + edge_list = backend_coupling_graph.edge_list() + removed_edge = [] + for edge in edge_list: + if set(edge) in removed_edge: + continue + try: + backend_coupling_graph.remove_edge(edge[0], edge[1]) + removed_edge.append(set(edge)) + except NoEdgeBetweenNodes: + pass + ``` + + Returns: + G_new (PyGraph): The graph with IBM backend compliant zigzag pattern. + num_alpha_beta_qubits (int): Number of connecting qubits between the linear chains + in the zigzag pattern. While we want as many connecting (alpha-beta) qubits between + the linear (alpha-alpha) chains, we cannot accomodate all due to qubit and connectivity + constraints of backends. This is the maximum number of connecting qubits the zigzag pattern + can have while being backend compliant (i.e., isomorphic to backend coupling graph). + """ + isomorphic = False + G = _create_two_linear_chains(num_orbitals=num_orbitals) + + G_new = copy.deepcopy(G) # to avoid not bound warning + while not isomorphic: + print("Inside while loop") + G_new = copy.deepcopy(G) + + if not alpha_beta_indices: + break + + # add new nodes and edges + for i, (a, b) in enumerate(sorted(alpha_beta_indices, key=lambda x: x[0])): + # print(f'i={i} | (alpha, beta)={(a, b)}') + if topology == "heavy-hex": + new_node = 2 * num_orbitals + i + G_new.add_node(new_node) + G_new.add_edge(a, new_node, None) + G_new.add_edge(new_node, b + num_orbitals, None) + elif topology == "grid": + G_new.add_edge(a, b + num_orbitals, None) + # pass + else: + raise ValueError(f"topology={topology} not allowed.") + # num_alpha_beta_qubits = num_alpha_beta_qubits + 1 + isomorphic = rustworkx.is_subgraph_isomorphic(backend_coupling_graph, G_new, call_limit=1_000_000) + + if not isomorphic: + print( + f"Backend cannot accomodate alpha_beta_incides {alpha_beta_indices}.\n " + f"Removing interaction {alpha_beta_indices[-1]} from the end." + ) + del alpha_beta_indices[-1] + + return G_new, alpha_beta_indices + +def _make_backend_cmap_pygraph(backend: BackendV2) -> PyGraph: + graph = backend.coupling_map.graph + if not graph.is_symmetric(): + graph.make_symmetric() + backend_coupling_graph = graph.to_undirected() + + edge_list = backend_coupling_graph.edge_list() + removed_edge = [] + for edge in edge_list: + if set(edge) in removed_edge: + continue + try: + backend_coupling_graph.remove_edge(edge[0], edge[1]) + removed_edge.append(set(edge)) + except NoEdgeBetweenNodes: + pass + + return backend_coupling_graph + + +def get_placeholder_initial_layout_and_allowed_alpha_beta_indices( + backend: BackendV2, + num_orbitals: int, + topology: str, + requested_alpha_beta_indices: Sequence[tuple[int, int]], +) -> tuple[list[int], list[tuple[int, int]]]: + """The main function that generates the zigzag pattern with physical qubits that can be used + as an `intial_layout` in a preset passmanager/transpiler. + + Args: + num_orbitals (int): Number of orbitals. + backend (BackendV2): A backend. + expected_alpha_beta_indices (list): User-defined arbitrary alpha-beta interactions. + Due to HW limitations, the full `expected` list of interactions may not be + accomodated. In that case, interaction pair from the end of the list is removed + one-by-one. Thus, a user must order the list in a descending order of priority. + score_layouts (bool): Optional. If `True`, it uses the `lightweight_layout_error_scoring` + function to score the isomorphic layouts and returns the layout with less errorneous qubits. + If `False`, returns the first isomorphic subgraph. + + Returns: + A tuple of device compliant layout (list[int]) with zigzag pattern and an int representing + number of alpha-beta-interactions. + """ + backend_coupling_graph = _make_backend_cmap_pygraph(backend=backend) + + G, allowed_alpha_beta_indices = get_layout_graph_and_allowed_alpha_beta_indices( + num_orbitals=num_orbitals, + backend_coupling_graph=backend_coupling_graph, + topology=topology, + alpha_beta_indices=list(requested_alpha_beta_indices), + ) + num_allowed_alpha_beta_indices = len(allowed_alpha_beta_indices) + isomorphic_mappings = rustworkx.vf2_mapping( + backend_coupling_graph, G, subgraph=True + ) + + mapping = next(isomorphic_mappings) + + if topology == "heavy-hex": + initial_layout= [-1] * (2 * num_orbitals + num_allowed_alpha_beta_indices) + elif topology == "grid": + initial_layout= [-1] * (2 * num_orbitals) + else: + raise ValueError("topology") + + for key, value in mapping.items(): + initial_layout[value] = key + + if -1 in initial_layout: + raise ValueError( + f"Negative qubit index in `initial_layout`. " + f"intial_layout={initial_layout}" + ) + + if topology == "grid": + return initial_layout, allowed_alpha_beta_indices + + return initial_layout[:-num_allowed_alpha_beta_indices], allowed_alpha_beta_indices + + +def get_pass_manager_and_allowed_alpha_beta_indices_for_lucj( + backend: BackendV2, + num_orbitals: int, + topology: Literal["grid", "heavy-hex"], + requested_alpha_beta_indices: Sequence[tuple[int, int]] | None, + **qiskit_pm_kwargs +) -> tuple[StagedPassManager, list[tuple[int, int]]]: + if "initial_layout" in qiskit_pm_kwargs: + warnings.warn("Argument `initial_layout` is ignored.") + del qiskit_pm_kwargs["initial_layout"] + + if "layout_method" in qiskit_pm_kwargs: + warnings.warn("Argument `layout_method` is ignored.") + del qiskit_pm_kwargs["layout_method"] + + if requested_alpha_beta_indices is None: + if topology == "heavy-hex": + requested_alpha_beta_indices = [ + (p, p) for p in range(num_orbitals) if p % 4 == 0 + ] + elif topology == "grid": + requested_alpha_beta_indices = [(p, p) for p in range(num_orbitals)] + else: + raise ValueError( + f"topology={topology} not recognized. " + f"Only 'heavy-hex' or 'grid' is allowed" + ) + + ( + placeholder_initial_layout, + allowed_alpha_beta_indices + ) = get_placeholder_initial_layout_and_allowed_alpha_beta_indices( + backend=backend, + num_orbitals=num_orbitals, + topology=topology, + requested_alpha_beta_indices=requested_alpha_beta_indices, + ) + + pm = generate_preset_pass_manager( + backend=backend, + initial_layout=placeholder_initial_layout, + **qiskit_pm_kwargs + ) + pm.pre_init = PRE_INIT + + def _custom_apply_post_layout_condition(property_set: dict[str, Any]) -> bool: + return property_set["post_layout"] is not None + + pm.routing.append(VF2PostLayout(target=backend.target, strict_direction=False)) + pm.routing.append( + ConditionalController( + ApplyLayout(), + condition=_custom_apply_post_layout_condition + ) + ) + + return pm, allowed_alpha_beta_indices + +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler import CouplingMap + +if __name__ == "__main__": + import ffsim + import numpy as np + import warnings + + warnings.filterwarnings("ignore") + + from qiskit import QuantumCircuit, QuantumRegister + from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + from qiskit_ibm_runtime import QiskitRuntimeService + + # common args + num_orbitals = 36 + requested_alpha_beta_indices = [ + (32, 32), (4, 4), (8, 8), (24, 24), (16, 16), (28, 28) + ] + n_reps = 2 + alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)] + + # heavy hex + print("\nHeavy Hex ...") + backend = FakeSherbrooke() + pm, alpha_beta_indices = get_pass_manager_and_allowed_alpha_beta_indices_for_lucj( + backend=backend, + num_orbitals=num_orbitals, + topology="heavy-hex", + requested_alpha_beta_indices=requested_alpha_beta_indices, + optimization_level=3 + ) + + num_alpha_beta_indices = len(alpha_beta_indices) + print(f'Final alpha-beta-interactions {alpha_beta_indices}') + ucj_op = ffsim.random.random_ucj_op_spin_balanced( + norb=num_orbitals, + n_reps=n_reps, + interaction_pairs=(alpha_alpha_indices, alpha_beta_indices), + seed=0 + ) + + qubits = QuantumRegister(2 * num_orbitals, name="q") + circuit = QuantumCircuit(qubits) + nelec = (5, 5) + circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) + + # apply the UCJ operator to the reference state + circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) + circuit.measure_all() + + isa_circuit_vf2 = pm.run(circuit) + num_2q_1 = isa_circuit_vf2.count_ops()["ecr"] + print(f"Num 2Q gates: {num_2q_1}") + print("Initial layout using the automated method: ") + print(isa_circuit_vf2.layout.initial_index_layout()[:2 * num_orbitals]) + + + # grid + print("\nGrid ...") + requested_alpha_beta_indices = ( + [(p, p) for p in range(11)] + [(p, p) for p in range(25, 36)] + ) + grid_cmap = CouplingMap.from_grid(num_rows=12, num_columns=10, bidirectional=True) + backend_grid = GenericBackendV2( + num_qubits=grid_cmap.size(), + basis_gates=["id", "rz", "sx", "x", "cz"], + coupling_map=grid_cmap, + ) + + pm, alpha_beta_indices = get_pass_manager_and_allowed_alpha_beta_indices_for_lucj( + backend=backend_grid, + num_orbitals=num_orbitals, + topology="grid", + requested_alpha_beta_indices=requested_alpha_beta_indices, + optimization_level=3 + ) + + num_alpha_beta_indices = len(alpha_beta_indices) + print(f'Final alpha-beta-interactions {alpha_beta_indices}') + ucj_op = ffsim.random.random_ucj_op_spin_balanced( + norb=num_orbitals, + n_reps=n_reps, + interaction_pairs=(alpha_alpha_indices, alpha_beta_indices), + seed=0 + ) + + qubits = QuantumRegister(2 * num_orbitals, name="q") + circuit = QuantumCircuit(qubits) + nelec = (5, 5) + circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) + + # apply the UCJ operator to the reference state + circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) + circuit.measure_all() + + isa_circuit_vf2 = pm.run(circuit) + num_2q_1 = isa_circuit_vf2.count_ops()["cz"] + print(f"Num 2Q gates: {num_2q_1}") + print("Initial layout using the automated method: ") + print(isa_circuit_vf2.layout.initial_index_layout()[:2 * num_orbitals]) \ No newline at end of file From e7e47db8b2ca697df955bf8a04e13c1c5ad7cc50 Mon Sep 17 00:00:00 2001 From: ashsaki Date: Fri, 15 Aug 2025 18:41:04 -0400 Subject: [PATCH 2/5] automatic LUCJ mapping pass on heavy-hex topology --- python/ffsim/qiskit/__init__.py | 9 +- .../qiskit/transpiler_passes/__init__.py | 4 + .../lucj_heavy_hex_preset_pass_manager.py | 282 +++++++++++++ .../ffsim/qiskit/transpiler_passes/lucj_pm.py | 380 ------------------ ...lucj_heavy_hex_preset_pass_manager_test.py | 94 +++++ 5 files changed, 386 insertions(+), 383 deletions(-) create mode 100644 python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py delete mode 100644 python/ffsim/qiskit/transpiler_passes/lucj_pm.py create mode 100644 tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py diff --git a/python/ffsim/qiskit/__init__.py b/python/ffsim/qiskit/__init__.py index be69b6bfc..9411f076c 100644 --- a/python/ffsim/qiskit/__init__.py +++ b/python/ffsim/qiskit/__init__.py @@ -35,7 +35,11 @@ from ffsim.qiskit.jordan_wigner import jordan_wigner from ffsim.qiskit.sampler import FfsimSampler from ffsim.qiskit.sim import final_state_vector -from ffsim.qiskit.transpiler_passes import DropNegligible, MergeOrbitalRotations +from ffsim.qiskit.transpiler_passes import ( + DropNegligible, + MergeOrbitalRotations, + generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas, +) from ffsim.qiskit.transpiler_stages import pre_init_passes from ffsim.qiskit.util import ffsim_vec_to_qiskit_vec, qiskit_vec_to_ffsim_vec @@ -45,8 +49,6 @@ See :func:`pre_init_passes` for a description of the transpiler passes included in this pass manager. """ - - __all__ = [ "DiagCoulombEvolutionJW", "DiagCoulombEvolutionSpinlessJW", @@ -72,6 +74,7 @@ "UCJOpSpinlessJW", "ffsim_vec_to_qiskit_vec", "final_state_vector", + "generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas", "jordan_wigner", "pre_init_passes", "qiskit_vec_to_ffsim_vec", diff --git a/python/ffsim/qiskit/transpiler_passes/__init__.py b/python/ffsim/qiskit/transpiler_passes/__init__.py index 83826a0f1..c29914542 100644 --- a/python/ffsim/qiskit/transpiler_passes/__init__.py +++ b/python/ffsim/qiskit/transpiler_passes/__init__.py @@ -11,9 +11,13 @@ """Qiskit transpiler passes for fermionic quantum circuits.""" from ffsim.qiskit.transpiler_passes.drop_negligible import DropNegligible +from ffsim.qiskit.transpiler_passes.lucj_heavy_hex_preset_pass_manager import ( + generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas, +) from ffsim.qiskit.transpiler_passes.merge_orbital_rotations import MergeOrbitalRotations __all__ = [ "DropNegligible", "MergeOrbitalRotations", + "generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas", ] diff --git a/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py new file mode 100644 index 000000000..e3cc33b05 --- /dev/null +++ b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py @@ -0,0 +1,282 @@ +import copy +import warnings +from typing import Any, Sequence + +import rustworkx +from qiskit.passmanager.flow_controllers import ConditionalController +from qiskit.providers import BackendV2 +from qiskit.transpiler import ( + StagedPassManager, + generate_preset_pass_manager, +) +from qiskit.transpiler.passes import ApplyLayout, VF2PostLayout +from rustworkx import NoEdgeBetweenNodes, PyGraph + +import ffsim + + +def _create_two_linear_chains(num_orbitals: int) -> PyGraph: + """In zig-zag layout, there are two linear chains (with connecting qubits between + the chains). This function creates those two linear chains which is a rustworkx + PyGraph with two disconnected linear chains. Each chain contains `num_orbitals` + number of nodes, i.e., in the final graph there are `2 * num_orbitals` number of + nodes. + + Args: + num_orbitals: Number orbitals or nodes in each linear chain. They are + also known as alpha-alpha interaction qubits. + + Returns: + A rustworkx.PyGraph with two disconnected linear chains each with `num_orbitals` + number of nodes. + """ + graph = rustworkx.PyGraph() + + for n in range(num_orbitals): + graph.add_node(n) + + for n in range(num_orbitals - 1): + graph.add_edge(n, n + 1, None) + + for n in range(num_orbitals, 2 * num_orbitals): + graph.add_node(n) + + for n in range(num_orbitals, 2 * num_orbitals - 1): + graph.add_edge(n, n + 1, None) + + return graph + + +def _get_layout_graph_and_allowed_alpha_beta_indices( + num_orbitals: int, + backend_coupling_graph: PyGraph, + alpha_beta_indices: list[tuple[int, int]], +) -> tuple[PyGraph, list[tuple[int, int]]]: + """This function creates the complete zigzag graph that _can be mapped_ to a IBM + QPU with heavy-hex connectivity (i.e., the zigzag pattern is an isomorphic + sub-graph to the QPU/backend coupling graph). The zigzag pattern includes + both linear chains (alpha-alpha/beta-beta interactions) and connecting qubits + between the linear chains (alpha-beta interactions). + + The algorithm works as follows: It starts with an interm graph (`graph_new`) + that has two linear chains with connecting nodes between two nodes (qubits) + specified by `alpha_beta_indices` list. The algorithm checks if the starting + graph is an isomorphic subgraph to the larger `backend_cpupling_graph`. If yes, + the routine ends and returns the `graph_new`. If not, it removes an alpha-beta + interaction pair from the end of list `alpha_beta_indices` and checks for + subgraph isomorphism again. It cycle continues, until a isomorhic subgraph is + found. + + Args: + num_orbitals: Number of orbitals, i.e., number of nodes in each alpha-alpha + linear chain. + backend_coupling_graph: The coupling graph of the backend on which the LUCJ + ansatz will be mapped and run. This function takes the coupling graph as + a undirected `rustworkx.PyGraph` where there is only one 'undirected' edge + between two nodes, i.e., qubits. Usually, the coupling graph of a IBM + backend is directed (e.g., Eagle devices such as `ibm_brisbane`) or may + have two edges between same two nodes (e.g., Heron `ibm_torino`). This + function is only compatible with undirected graphs where there is only + a single undirected edge between same two nodes. + + Returns: + graph_new: A graph that has the _zigzag_ pattern and is an isomorphic subgraph + to an a heavy-hex IBM backend. + num_alpha_beta_qubits: Number of connecting qubits between the linear chains + in the zigzag pattern. While we want as many connecting (alpha-beta) qubits + between the linear (alpha-alpha) chains, we cannot accomodate all due to + connectivity constraints of backends. This is the maximum number of + connecting qubits the zigzag pattern can have while being backend compliant + (i.e., isomorphic subgraph to the backend coupling graph). + """ + isomorphic = False + graph = _create_two_linear_chains(num_orbitals=num_orbitals) + + graph_new = copy.deepcopy(graph) # to avoid not bound warning + while not isomorphic: + graph_new = copy.deepcopy(graph) + + if not alpha_beta_indices: + break + + # add new nodes and edges + for i, (a, b) in enumerate(sorted(alpha_beta_indices, key=lambda x: x[0])): + new_node = 2 * num_orbitals + i + graph_new.add_node(new_node) + graph_new.add_edge(a, new_node, None) + graph_new.add_edge(new_node, b + num_orbitals, None) + + isomorphic = rustworkx.is_subgraph_isomorphic( + backend_coupling_graph, + graph_new, + call_limit=1_000_000, + id_order=False, + induced=False, + ) + + if not isomorphic: + warnings.warn( + f"Backend cannot accomodate alpha_beta_incides {alpha_beta_indices}.\n " + f"Removing interaction {alpha_beta_indices[-1]} from the end." + ) + del alpha_beta_indices[-1] + + return graph_new, alpha_beta_indices + + +def _make_backend_cmap_pygraph(backend: BackendV2) -> PyGraph: + """Converts an IBM backend coupling map to an undirected rustworkx.PyGraph where + there is only a single edge between same two nodes. + + Args: + backend: An IBM backend. + + Returns: + A rustworkx.PyGraph with a single undirected edge between same two nodes. + """ + graph = backend.coupling_map.graph + if not graph.is_symmetric(): + graph.make_symmetric() + backend_coupling_graph = graph.to_undirected() + + edge_list = backend_coupling_graph.edge_list() + removed_edge = [] + for edge in edge_list: + if set(edge) in removed_edge: + continue + try: + backend_coupling_graph.remove_edge(edge[0], edge[1]) + removed_edge.append(set(edge)) + except NoEdgeBetweenNodes: + pass + + return backend_coupling_graph + + +def _get_placeholder_initial_layout_and_allowed_alpha_beta_indices( + backend: BackendV2, + num_orbitals: int, + requested_alpha_beta_indices: Sequence[tuple[int, int]], +) -> tuple[list[int], list[tuple[int, int]]]: + """The main function that generates the zigzag pattern with physical qubits that + can be used as an `intial_layout` in a preset passmanager/transpiler. + + Args: + num_orbitals: Number of orbitals. + backend: A backend. + requested_alpha_beta_indices: A list of requested alpha-beta interactions. + Due to HW limitations, the full requested list of interactions may not be + accomodated. In that case, interaction pair from the end of the list is + removed one-by-one. Thus, if user specified, order the list in a descending + priority. + + Returns: + A tuple of device compliant layout (`list[int]`) with zigzag pattern and an + `int` representing number of alpha-beta-interactions. + """ + backend_coupling_graph = _make_backend_cmap_pygraph(backend=backend) + + (graph, allowed_alpha_beta_indices) = ( + _get_layout_graph_and_allowed_alpha_beta_indices( + num_orbitals=num_orbitals, + backend_coupling_graph=backend_coupling_graph, + alpha_beta_indices=list(requested_alpha_beta_indices), + ) + ) + num_allowed_alpha_beta_indices = len(allowed_alpha_beta_indices) + isomorphic_mappings = rustworkx.vf2_mapping( + backend_coupling_graph, graph, subgraph=True, id_order=False, induced=False + ) + + mapping = next(isomorphic_mappings) + initial_layout = [-1] * (2 * num_orbitals + num_allowed_alpha_beta_indices) + + for key, value in mapping.items(): + initial_layout[value] = key + + if -1 in initial_layout: + raise ValueError( + f"Not all qubits in the placeholder `initial_layout` is properly set." + f"Negative qubit index in `initial_layout`. " + f"intial_layout={initial_layout}" + ) + + return initial_layout[:-num_allowed_alpha_beta_indices], allowed_alpha_beta_indices + + +def generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( + backend: BackendV2, + num_orbitals: int, + requested_alpha_beta_indices: Sequence[tuple[int, int]] | None = None, + **qiskit_pm_kwargs, +) -> tuple[StagedPassManager, list[tuple[int, int]]]: + """Generates a Qiskit preset pass manager that adheres to local + unitary-coupled Jastrow (LUCJ) anstaz's _zigzag_ layout on heavy-hex + backend topologies (Mario Motta et al., + https://pubs.rsc.org/en/content/articlehtml/2023/sc/d3sc02516k). + In addition to the pass manager, this function also returns a list of + hardware compatible alpha-beta interactions. + + Args: + backend: An IBM backend. + num_orbitals: The number of _spatial_ orbitals of the molecule to be + mapped using the LUCJ ansatz. The number of qubits in the LUCJ + ansatz will be 2 * num_orbitals + ancilae qubits. + requested_alpha_beta_indices: A user may optionally request a list of + alpha-beta interactions. The code will try to find a layout that satisfies + the user requested alpha-beta pairs. However, due to limited hardware + connectivity, the request may not be entirely entertained. It that case, + the code removes pairs from the end of the requested list one-by-one from + the end of the list until a layout is found. Therefore, a user should list + the pairs in desceding order of priority. If `None`, the code uses + sequential alpha-beta interactions, i.e., [(0, 0), (4, 4), ... up to + allowed by backend connectivity]. + Default: `None`. + **qiskit_pm_kwargs: The function accepts full list of arguments from + [`qiskit.transpiler.generate_preset_pass_manager`](https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.transpiler.generate_preset_pass_manager) + except `initial_layout` and `layout_method` as they are conflicting with this + routine's functionality. + If specified, they will be deleted with a warning. + + Returns: + pm: A preset pass manager. + allowed_alpha_beta_indices: A list of alpha-beta pairs that can be accomodated + on the backend. + """ # noqa: E501 + if "initial_layout" in qiskit_pm_kwargs: + warnings.warn("Argument `initial_layout` is ignored.") + del qiskit_pm_kwargs["initial_layout"] + + if "layout_method" in qiskit_pm_kwargs: + warnings.warn("Argument `layout_method` is ignored.") + del qiskit_pm_kwargs["layout_method"] + + if requested_alpha_beta_indices is None: + requested_alpha_beta_indices = [ + (p, p) for p in range(num_orbitals) if p % 4 == 0 + ] + + (placeholder_initial_layout, allowed_alpha_beta_indices) = ( + _get_placeholder_initial_layout_and_allowed_alpha_beta_indices( + backend=backend, + num_orbitals=num_orbitals, + requested_alpha_beta_indices=requested_alpha_beta_indices, + ) + ) + + pm = generate_preset_pass_manager( + backend=backend, initial_layout=placeholder_initial_layout, **qiskit_pm_kwargs + ) + pm.pre_init = ffsim.qiskit.PRE_INIT + + def _custom_apply_post_layout_condition(property_set: dict[str, Any]) -> bool: + return property_set["post_layout"] is not None + + pm.routing.append(VF2PostLayout(target=backend.target, strict_direction=False)) + pm.routing.append( + ConditionalController( + ApplyLayout(), condition=_custom_apply_post_layout_condition + ) + ) + + return pm, allowed_alpha_beta_indices diff --git a/python/ffsim/qiskit/transpiler_passes/lucj_pm.py b/python/ffsim/qiskit/transpiler_passes/lucj_pm.py deleted file mode 100644 index 9bfcedd0f..000000000 --- a/python/ffsim/qiskit/transpiler_passes/lucj_pm.py +++ /dev/null @@ -1,380 +0,0 @@ -import warnings -from qiskit.providers import BackendV2 -from typing import Literal, Sequence, Any -from qiskit import QuantumCircuit -from qiskit.transpiler import generate_preset_pass_manager, StagedPassManager -from qiskit.transpiler.passes import VF2PostLayout, ApplyLayout -from qiskit.passmanager.flow_controllers import ConditionalController -from qiskit.transpiler import PassManager - -import copy -from typing import Sequence - -from ffsim.qiskit import PRE_INIT - -import rustworkx -from qiskit.providers import BackendV2 -from qiskit_ibm_runtime.fake_provider import FakeSherbrooke, FakeTorino -from rustworkx import NoEdgeBetweenNodes, PyGraph - - -"""Funtionality - -1. Throw warning if qiskit transpiler's intial_layout and layout_method are specified. - These two directly conflicts with LUCJ's need for custom layout -2. -""" - -def _create_two_linear_chains(num_orbitals: int) -> PyGraph: - """In zig-zag layout, there are two linear chains (with connecting qubits between - the chains). This function creates those two linear chains: a rustworkx PyGraph - with two disconnected linear chains. Each chain contains `num_orbitals` number - of nodes, i.e., in the final graph there are `2 * num_orbitals` number of nodes. - - Args: - num_orbitals (int): Number orbitals or nodes in each linear chain. They are - also known as alpha-alpha interaction qubits. - - Returns: - A rustworkx.PyGraph with two disconnected linear chains each with `num_orbitals` - number of nodes. - """ - G = rustworkx.PyGraph() - - for n in range(num_orbitals): - G.add_node(n) - - for n in range(num_orbitals - 1): - G.add_edge(n, n + 1, None) - - for n in range(num_orbitals, 2 * num_orbitals): - G.add_node(n) - - for n in range(num_orbitals, 2 * num_orbitals - 1): - G.add_edge(n, n + 1, None) - - return G - -def get_layout_graph_and_allowed_alpha_beta_indices( - num_orbitals: int, - backend_coupling_graph: PyGraph, - topology: str, - alpha_beta_indices: list[tuple[int, int]], -) -> tuple[PyGraph, list[tuple[int, int]]]: - """This function creates the complete zigzag graph that 'can be mapped' to a IBM QPU with - heavy-hex connectivity (the zigzag must be an isomorphic sub-graph to the QPU/backend - coupling graph for it to be mapped). - The zigzag pattern includes both linear chains (alpha-alpha interactions) and connecting - qubits between the linear chains (alpha-beta interactions). - - Args: - num_orbitals (int): Number of orbitals, i.e., number of nodes in each alpha-alpha linear chain. - backend_coupling_graph (PyGraph): The coupling graph of the backend on which the LUCJ ansatz - will be mapped and run. This function takes the coupling graph as a undirected - `rustworkx.PyGraph` where there is only one 'undirected' edge between two nodes, - i.e., qubits. Usually, the coupling graph of a IBM backend is directed (e.g., Eagle devices - such as ibm_sherbrooke) or may have two edges between two nodes (e.g., Heron `ibm_torino`). - A user needs to be make such graphs undirected and/or remove duplicate edges to make them - compatible with this function. One way to do this is as follows: - ``` - graph = backend.coupling_map.graph - if not graph.is_symmetric(): - graph.make_symmetric() - backend_coupling_graph = graph.to_undirected() - - edge_list = backend_coupling_graph.edge_list() - removed_edge = [] - for edge in edge_list: - if set(edge) in removed_edge: - continue - try: - backend_coupling_graph.remove_edge(edge[0], edge[1]) - removed_edge.append(set(edge)) - except NoEdgeBetweenNodes: - pass - ``` - - Returns: - G_new (PyGraph): The graph with IBM backend compliant zigzag pattern. - num_alpha_beta_qubits (int): Number of connecting qubits between the linear chains - in the zigzag pattern. While we want as many connecting (alpha-beta) qubits between - the linear (alpha-alpha) chains, we cannot accomodate all due to qubit and connectivity - constraints of backends. This is the maximum number of connecting qubits the zigzag pattern - can have while being backend compliant (i.e., isomorphic to backend coupling graph). - """ - isomorphic = False - G = _create_two_linear_chains(num_orbitals=num_orbitals) - - G_new = copy.deepcopy(G) # to avoid not bound warning - while not isomorphic: - print("Inside while loop") - G_new = copy.deepcopy(G) - - if not alpha_beta_indices: - break - - # add new nodes and edges - for i, (a, b) in enumerate(sorted(alpha_beta_indices, key=lambda x: x[0])): - # print(f'i={i} | (alpha, beta)={(a, b)}') - if topology == "heavy-hex": - new_node = 2 * num_orbitals + i - G_new.add_node(new_node) - G_new.add_edge(a, new_node, None) - G_new.add_edge(new_node, b + num_orbitals, None) - elif topology == "grid": - G_new.add_edge(a, b + num_orbitals, None) - # pass - else: - raise ValueError(f"topology={topology} not allowed.") - # num_alpha_beta_qubits = num_alpha_beta_qubits + 1 - isomorphic = rustworkx.is_subgraph_isomorphic(backend_coupling_graph, G_new, call_limit=1_000_000) - - if not isomorphic: - print( - f"Backend cannot accomodate alpha_beta_incides {alpha_beta_indices}.\n " - f"Removing interaction {alpha_beta_indices[-1]} from the end." - ) - del alpha_beta_indices[-1] - - return G_new, alpha_beta_indices - -def _make_backend_cmap_pygraph(backend: BackendV2) -> PyGraph: - graph = backend.coupling_map.graph - if not graph.is_symmetric(): - graph.make_symmetric() - backend_coupling_graph = graph.to_undirected() - - edge_list = backend_coupling_graph.edge_list() - removed_edge = [] - for edge in edge_list: - if set(edge) in removed_edge: - continue - try: - backend_coupling_graph.remove_edge(edge[0], edge[1]) - removed_edge.append(set(edge)) - except NoEdgeBetweenNodes: - pass - - return backend_coupling_graph - - -def get_placeholder_initial_layout_and_allowed_alpha_beta_indices( - backend: BackendV2, - num_orbitals: int, - topology: str, - requested_alpha_beta_indices: Sequence[tuple[int, int]], -) -> tuple[list[int], list[tuple[int, int]]]: - """The main function that generates the zigzag pattern with physical qubits that can be used - as an `intial_layout` in a preset passmanager/transpiler. - - Args: - num_orbitals (int): Number of orbitals. - backend (BackendV2): A backend. - expected_alpha_beta_indices (list): User-defined arbitrary alpha-beta interactions. - Due to HW limitations, the full `expected` list of interactions may not be - accomodated. In that case, interaction pair from the end of the list is removed - one-by-one. Thus, a user must order the list in a descending order of priority. - score_layouts (bool): Optional. If `True`, it uses the `lightweight_layout_error_scoring` - function to score the isomorphic layouts and returns the layout with less errorneous qubits. - If `False`, returns the first isomorphic subgraph. - - Returns: - A tuple of device compliant layout (list[int]) with zigzag pattern and an int representing - number of alpha-beta-interactions. - """ - backend_coupling_graph = _make_backend_cmap_pygraph(backend=backend) - - G, allowed_alpha_beta_indices = get_layout_graph_and_allowed_alpha_beta_indices( - num_orbitals=num_orbitals, - backend_coupling_graph=backend_coupling_graph, - topology=topology, - alpha_beta_indices=list(requested_alpha_beta_indices), - ) - num_allowed_alpha_beta_indices = len(allowed_alpha_beta_indices) - isomorphic_mappings = rustworkx.vf2_mapping( - backend_coupling_graph, G, subgraph=True - ) - - mapping = next(isomorphic_mappings) - - if topology == "heavy-hex": - initial_layout= [-1] * (2 * num_orbitals + num_allowed_alpha_beta_indices) - elif topology == "grid": - initial_layout= [-1] * (2 * num_orbitals) - else: - raise ValueError("topology") - - for key, value in mapping.items(): - initial_layout[value] = key - - if -1 in initial_layout: - raise ValueError( - f"Negative qubit index in `initial_layout`. " - f"intial_layout={initial_layout}" - ) - - if topology == "grid": - return initial_layout, allowed_alpha_beta_indices - - return initial_layout[:-num_allowed_alpha_beta_indices], allowed_alpha_beta_indices - - -def get_pass_manager_and_allowed_alpha_beta_indices_for_lucj( - backend: BackendV2, - num_orbitals: int, - topology: Literal["grid", "heavy-hex"], - requested_alpha_beta_indices: Sequence[tuple[int, int]] | None, - **qiskit_pm_kwargs -) -> tuple[StagedPassManager, list[tuple[int, int]]]: - if "initial_layout" in qiskit_pm_kwargs: - warnings.warn("Argument `initial_layout` is ignored.") - del qiskit_pm_kwargs["initial_layout"] - - if "layout_method" in qiskit_pm_kwargs: - warnings.warn("Argument `layout_method` is ignored.") - del qiskit_pm_kwargs["layout_method"] - - if requested_alpha_beta_indices is None: - if topology == "heavy-hex": - requested_alpha_beta_indices = [ - (p, p) for p in range(num_orbitals) if p % 4 == 0 - ] - elif topology == "grid": - requested_alpha_beta_indices = [(p, p) for p in range(num_orbitals)] - else: - raise ValueError( - f"topology={topology} not recognized. " - f"Only 'heavy-hex' or 'grid' is allowed" - ) - - ( - placeholder_initial_layout, - allowed_alpha_beta_indices - ) = get_placeholder_initial_layout_and_allowed_alpha_beta_indices( - backend=backend, - num_orbitals=num_orbitals, - topology=topology, - requested_alpha_beta_indices=requested_alpha_beta_indices, - ) - - pm = generate_preset_pass_manager( - backend=backend, - initial_layout=placeholder_initial_layout, - **qiskit_pm_kwargs - ) - pm.pre_init = PRE_INIT - - def _custom_apply_post_layout_condition(property_set: dict[str, Any]) -> bool: - return property_set["post_layout"] is not None - - pm.routing.append(VF2PostLayout(target=backend.target, strict_direction=False)) - pm.routing.append( - ConditionalController( - ApplyLayout(), - condition=_custom_apply_post_layout_condition - ) - ) - - return pm, allowed_alpha_beta_indices - -from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.transpiler import CouplingMap - -if __name__ == "__main__": - import ffsim - import numpy as np - import warnings - - warnings.filterwarnings("ignore") - - from qiskit import QuantumCircuit, QuantumRegister - from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager - from qiskit_ibm_runtime import QiskitRuntimeService - - # common args - num_orbitals = 36 - requested_alpha_beta_indices = [ - (32, 32), (4, 4), (8, 8), (24, 24), (16, 16), (28, 28) - ] - n_reps = 2 - alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)] - - # heavy hex - print("\nHeavy Hex ...") - backend = FakeSherbrooke() - pm, alpha_beta_indices = get_pass_manager_and_allowed_alpha_beta_indices_for_lucj( - backend=backend, - num_orbitals=num_orbitals, - topology="heavy-hex", - requested_alpha_beta_indices=requested_alpha_beta_indices, - optimization_level=3 - ) - - num_alpha_beta_indices = len(alpha_beta_indices) - print(f'Final alpha-beta-interactions {alpha_beta_indices}') - ucj_op = ffsim.random.random_ucj_op_spin_balanced( - norb=num_orbitals, - n_reps=n_reps, - interaction_pairs=(alpha_alpha_indices, alpha_beta_indices), - seed=0 - ) - - qubits = QuantumRegister(2 * num_orbitals, name="q") - circuit = QuantumCircuit(qubits) - nelec = (5, 5) - circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) - - # apply the UCJ operator to the reference state - circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) - circuit.measure_all() - - isa_circuit_vf2 = pm.run(circuit) - num_2q_1 = isa_circuit_vf2.count_ops()["ecr"] - print(f"Num 2Q gates: {num_2q_1}") - print("Initial layout using the automated method: ") - print(isa_circuit_vf2.layout.initial_index_layout()[:2 * num_orbitals]) - - - # grid - print("\nGrid ...") - requested_alpha_beta_indices = ( - [(p, p) for p in range(11)] + [(p, p) for p in range(25, 36)] - ) - grid_cmap = CouplingMap.from_grid(num_rows=12, num_columns=10, bidirectional=True) - backend_grid = GenericBackendV2( - num_qubits=grid_cmap.size(), - basis_gates=["id", "rz", "sx", "x", "cz"], - coupling_map=grid_cmap, - ) - - pm, alpha_beta_indices = get_pass_manager_and_allowed_alpha_beta_indices_for_lucj( - backend=backend_grid, - num_orbitals=num_orbitals, - topology="grid", - requested_alpha_beta_indices=requested_alpha_beta_indices, - optimization_level=3 - ) - - num_alpha_beta_indices = len(alpha_beta_indices) - print(f'Final alpha-beta-interactions {alpha_beta_indices}') - ucj_op = ffsim.random.random_ucj_op_spin_balanced( - norb=num_orbitals, - n_reps=n_reps, - interaction_pairs=(alpha_alpha_indices, alpha_beta_indices), - seed=0 - ) - - qubits = QuantumRegister(2 * num_orbitals, name="q") - circuit = QuantumCircuit(qubits) - nelec = (5, 5) - circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) - - # apply the UCJ operator to the reference state - circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) - circuit.measure_all() - - isa_circuit_vf2 = pm.run(circuit) - num_2q_1 = isa_circuit_vf2.count_ops()["cz"] - print(f"Num 2Q gates: {num_2q_1}") - print("Initial layout using the automated method: ") - print(isa_circuit_vf2.layout.initial_index_layout()[:2 * num_orbitals]) \ No newline at end of file diff --git a/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py b/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py new file mode 100644 index 000000000..0d241d4eb --- /dev/null +++ b/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py @@ -0,0 +1,94 @@ +from qiskit_ibm_runtime.fake_provider import FakeMarrakesh, FakeSherbrooke + +from ffsim.qiskit import generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas + +if __name__ == "__main__": + import warnings + + import ffsim + + warnings.filterwarnings("ignore") + + from qiskit import QuantumCircuit, QuantumRegister + + # common args + num_orbitals = 36 + requested_alpha_beta_indices = [ + (32, 32), + (4, 4), + (8, 8), + (24, 24), + (16, 16), + (28, 28), + ] + n_reps = 2 + alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)] + + # heavy hex + print("\nHeavy Hex ...") + backend = FakeSherbrooke() + backend = FakeMarrakesh() + pm, alpha_beta_indices = ( + generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( + backend=backend, + num_orbitals=num_orbitals, + requested_alpha_beta_indices=requested_alpha_beta_indices, + optimization_level=3, + ) + ) + + num_alpha_beta_indices = len(alpha_beta_indices) + print(f"Final alpha-beta-interactions {alpha_beta_indices}") + ucj_op = ffsim.random.random_ucj_op_spin_balanced( + norb=num_orbitals, + n_reps=n_reps, + interaction_pairs=(alpha_alpha_indices, alpha_beta_indices), + seed=0, + ) + + qubits = QuantumRegister(2 * num_orbitals, name="q") + circuit = QuantumCircuit(qubits) + nelec = (5, 5) + circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) + + # apply the UCJ operator to the reference state + circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) + circuit.measure_all() + + isa_circuit_vf2 = pm.run(circuit) + num_2q_1 = isa_circuit_vf2.count_ops()["cz"] + print(f"Num 2Q gates: {num_2q_1}") + print("Initial layout using the automated method: ") + print(isa_circuit_vf2.layout.initial_index_layout()[: 2 * num_orbitals]) + + # no requested alpha-beta indices + print("No requested alpha-beta indices.\n") + pm, alpha_beta_indices = ( + generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( + backend=backend, num_orbitals=num_orbitals, optimization_level=3 + ) + ) + + num_alpha_beta_indices = len(alpha_beta_indices) + print(f"Final alpha-beta-interactions {alpha_beta_indices}") + ucj_op = ffsim.random.random_ucj_op_spin_balanced( + norb=num_orbitals, + n_reps=n_reps, + interaction_pairs=(alpha_alpha_indices, alpha_beta_indices), + seed=0, + ) + + qubits = QuantumRegister(2 * num_orbitals, name="q") + circuit = QuantumCircuit(qubits) + nelec = (5, 5) + circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) + + # apply the UCJ operator to the reference state + circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) + circuit.measure_all() + + isa_circuit_vf2 = pm.run(circuit) + num_2q_1 = isa_circuit_vf2.count_ops()["cz"] + print(f"Num 2Q gates: {num_2q_1}") + print("Initial layout using the automated method: ") + print(isa_circuit_vf2.layout.initial_index_layout()[: 2 * num_orbitals]) From a828b0b7519c1ef2578b4e479d75dc3064ba2e5a Mon Sep 17 00:00:00 2001 From: ashsaki Date: Mon, 22 Sep 2025 16:32:58 -0400 Subject: [PATCH 3/5] add tests and shorten func name --- python/ffsim/qiskit/__init__.py | 4 +- .../qiskit/transpiler_passes/__init__.py | 4 +- .../lucj_heavy_hex_preset_pass_manager.py | 21 ++- ...lucj_heavy_hex_preset_pass_manager_test.py | 137 ++++++++++-------- 4 files changed, 95 insertions(+), 71 deletions(-) diff --git a/python/ffsim/qiskit/__init__.py b/python/ffsim/qiskit/__init__.py index 9411f076c..0f6ee40f0 100644 --- a/python/ffsim/qiskit/__init__.py +++ b/python/ffsim/qiskit/__init__.py @@ -38,7 +38,7 @@ from ffsim.qiskit.transpiler_passes import ( DropNegligible, MergeOrbitalRotations, - generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas, + generate_pm_and_interactions_lucj_heavy_hex, ) from ffsim.qiskit.transpiler_stages import pre_init_passes from ffsim.qiskit.util import ffsim_vec_to_qiskit_vec, qiskit_vec_to_ffsim_vec @@ -74,7 +74,7 @@ "UCJOpSpinlessJW", "ffsim_vec_to_qiskit_vec", "final_state_vector", - "generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas", + "generate_pm_and_interactions_lucj_heavy_hex", "jordan_wigner", "pre_init_passes", "qiskit_vec_to_ffsim_vec", diff --git a/python/ffsim/qiskit/transpiler_passes/__init__.py b/python/ffsim/qiskit/transpiler_passes/__init__.py index c29914542..a40cd9b52 100644 --- a/python/ffsim/qiskit/transpiler_passes/__init__.py +++ b/python/ffsim/qiskit/transpiler_passes/__init__.py @@ -12,12 +12,12 @@ from ffsim.qiskit.transpiler_passes.drop_negligible import DropNegligible from ffsim.qiskit.transpiler_passes.lucj_heavy_hex_preset_pass_manager import ( - generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas, + generate_pm_and_interactions_lucj_heavy_hex, ) from ffsim.qiskit.transpiler_passes.merge_orbital_rotations import MergeOrbitalRotations __all__ = [ "DropNegligible", "MergeOrbitalRotations", - "generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas", + "generate_pm_and_interactions_lucj_heavy_hex", ] diff --git a/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py index e3cc33b05..2c7b3731f 100644 --- a/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py +++ b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py @@ -153,7 +153,7 @@ def _make_backend_cmap_pygraph(backend: BackendV2) -> PyGraph: return backend_coupling_graph -def _get_placeholder_initial_layout_and_allowed_alpha_beta_indices( +def _get_placeholder_layout_and_allowed_interactions( backend: BackendV2, num_orbitals: int, requested_alpha_beta_indices: Sequence[tuple[int, int]], @@ -204,7 +204,7 @@ def _get_placeholder_initial_layout_and_allowed_alpha_beta_indices( return initial_layout[:-num_allowed_alpha_beta_indices], allowed_alpha_beta_indices -def generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( +def generate_pm_and_interactions_lucj_heavy_hex( backend: BackendV2, num_orbitals: int, requested_alpha_beta_indices: Sequence[tuple[int, int]] | None = None, @@ -225,7 +225,7 @@ def generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( requested_alpha_beta_indices: A user may optionally request a list of alpha-beta interactions. The code will try to find a layout that satisfies the user requested alpha-beta pairs. However, due to limited hardware - connectivity, the request may not be entirely entertained. It that case, + connectivity, the request may not be entirely entertained. In that case, the code removes pairs from the end of the requested list one-by-one from the end of the list until a layout is found. Therefore, a user should list the pairs in desceding order of priority. If `None`, the code uses @@ -251,13 +251,21 @@ def generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( warnings.warn("Argument `layout_method` is ignored.") del qiskit_pm_kwargs["layout_method"] + if requested_alpha_beta_indices: + for alpha, beta in requested_alpha_beta_indices: + if alpha >= num_orbitals or beta >= num_orbitals: + raise ValueError( + f"Requested alpha-beta interaction {(alpha, beta)} is out of " + f"range for maximum spatial orbital index of {num_orbitals - 1}." + ) + if requested_alpha_beta_indices is None: requested_alpha_beta_indices = [ (p, p) for p in range(num_orbitals) if p % 4 == 0 ] (placeholder_initial_layout, allowed_alpha_beta_indices) = ( - _get_placeholder_initial_layout_and_allowed_alpha_beta_indices( + _get_placeholder_layout_and_allowed_interactions( backend=backend, num_orbitals=num_orbitals, requested_alpha_beta_indices=requested_alpha_beta_indices, @@ -269,6 +277,11 @@ def generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( ) pm.pre_init = ffsim.qiskit.PRE_INIT + # generating a preset pass manager with `initial_layout` + # (`=placeholder_initial_layout`) disables the `VF2PostLayout` pass. + # Therefore, we manually turn on the pass here so that it can search + # (better) isomorphic subgraph layouts to the initial layout and apply it + # to the circuit. def _custom_apply_post_layout_condition(property_set: dict[str, Any]) -> bool: return property_set["post_layout"] is not None diff --git a/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py b/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py index 0d241d4eb..7a439be3b 100644 --- a/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py +++ b/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py @@ -1,76 +1,71 @@ -from qiskit_ibm_runtime.fake_provider import FakeMarrakesh, FakeSherbrooke +import pytest +from qiskit import QuantumCircuit, QuantumRegister +from qiskit_ibm_runtime.fake_provider import FakeMarrakesh -from ffsim.qiskit import generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas +import ffsim +from ffsim.qiskit import generate_pm_and_interactions_lucj_heavy_hex -if __name__ == "__main__": - import warnings +backend = FakeMarrakesh() +num_orbitals = 36 - import ffsim - warnings.filterwarnings("ignore") +def test_raise_warning1(): + with pytest.warns(UserWarning, match="Argument `initial_layout` is ignored."): + _, _ = generate_pm_and_interactions_lucj_heavy_hex( + backend=backend, + num_orbitals=num_orbitals, + requested_alpha_beta_indices=None, + optimization_level=3, + initial_layout=[1], + ) - from qiskit import QuantumCircuit, QuantumRegister - # common args - num_orbitals = 36 - requested_alpha_beta_indices = [ - (32, 32), - (4, 4), - (8, 8), - (24, 24), - (16, 16), - (28, 28), - ] - n_reps = 2 - alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)] +def test_raise_warning2(): + with pytest.warns(UserWarning, match="Argument `layout_method` is ignored."): + _, _ = generate_pm_and_interactions_lucj_heavy_hex( + backend=backend, + num_orbitals=num_orbitals, + requested_alpha_beta_indices=None, + optimization_level=3, + layout_method="placeholder", + ) - # heavy hex - print("\nHeavy Hex ...") - backend = FakeSherbrooke() - backend = FakeMarrakesh() - pm, alpha_beta_indices = ( - generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( + +test_data1 = [[(num_orbitals + 1, num_orbitals + 1)], [(num_orbitals, num_orbitals)]] + + +@pytest.mark.parametrize("requested_alpha_beta_indices", test_data1) +def test_raise_value_error(requested_alpha_beta_indices): + with pytest.raises(ValueError): + _, _ = generate_pm_and_interactions_lucj_heavy_hex( backend=backend, num_orbitals=num_orbitals, requested_alpha_beta_indices=requested_alpha_beta_indices, optimization_level=3, ) - ) - num_alpha_beta_indices = len(alpha_beta_indices) - print(f"Final alpha-beta-interactions {alpha_beta_indices}") - ucj_op = ffsim.random.random_ucj_op_spin_balanced( - norb=num_orbitals, - n_reps=n_reps, - interaction_pairs=(alpha_alpha_indices, alpha_beta_indices), - seed=0, - ) - qubits = QuantumRegister(2 * num_orbitals, name="q") - circuit = QuantumCircuit(qubits) - nelec = (5, 5) - circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) +test_data2 = [None, [(32, 32), (4, 4), (8, 8), (24, 24), (16, 16), (28, 28)]] - # apply the UCJ operator to the reference state - circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) - circuit.measure_all() - isa_circuit_vf2 = pm.run(circuit) - num_2q_1 = isa_circuit_vf2.count_ops()["cz"] - print(f"Num 2Q gates: {num_2q_1}") - print("Initial layout using the automated method: ") - print(isa_circuit_vf2.layout.initial_index_layout()[: 2 * num_orbitals]) - - # no requested alpha-beta indices - print("No requested alpha-beta indices.\n") - pm, alpha_beta_indices = ( - generate_preset_pass_manager_lucj_heavy_hex_with_alpha_betas( - backend=backend, num_orbitals=num_orbitals, optimization_level=3 - ) +@pytest.mark.parametrize("requested_alpha_beta_indices", test_data2) +def test_generate_pm_and_interactions_lucj_heavy_hex(requested_alpha_beta_indices): + """Tests whether the LUCJ ansatz transpiled by the custom pass manager + retains the expected zig-zag qubit pattern. To be a zig-zag pattern, + 1. A qubit on the alpha (beta) chain have to 1 distance (edge) apart + from the next qubit on the chain. + 2. Distance between an alpha and a beta qubit connected by an ancilla + must be 2 (2 edges apart). + """ + n_reps = 2 + alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)] + pm, alpha_beta_indices = generate_pm_and_interactions_lucj_heavy_hex( + backend=backend, + num_orbitals=num_orbitals, + requested_alpha_beta_indices=requested_alpha_beta_indices, + optimization_level=3, ) - num_alpha_beta_indices = len(alpha_beta_indices) - print(f"Final alpha-beta-interactions {alpha_beta_indices}") ucj_op = ffsim.random.random_ucj_op_spin_balanced( norb=num_orbitals, n_reps=n_reps, @@ -78,17 +73,33 @@ seed=0, ) + nelec = (5, 5) + qubits = QuantumRegister(2 * num_orbitals, name="q") circuit = QuantumCircuit(qubits) - nelec = (5, 5) circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits) - - # apply the UCJ operator to the reference state circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits) circuit.measure_all() - isa_circuit_vf2 = pm.run(circuit) - num_2q_1 = isa_circuit_vf2.count_ops()["cz"] - print(f"Num 2Q gates: {num_2q_1}") - print("Initial layout using the automated method: ") - print(isa_circuit_vf2.layout.initial_index_layout()[: 2 * num_orbitals]) + isa_circuit = pm.run(circuit) + initial_layout = isa_circuit.layout.initial_index_layout(filter_ancillas=False) + alpha_qubits = initial_layout[:num_orbitals] + beta_qubits = initial_layout[num_orbitals : 2 * num_orbitals] + + coupling_map = backend.target.build_coupling_map() + + # alpha and beta qubits connected by an ancilla must be 2 edges apart + # alpha - ancilla - beta + for idx, _ in alpha_beta_indices: + dist = coupling_map.distance(alpha_qubits[idx], beta_qubits[idx]) + assert dist == 2 + + # Adjacent qubits on alpha/beta chains are 1 edge apart + for idx1 in range(num_orbitals - 1): + idx2 = idx1 + 1 + + dist = coupling_map.distance(alpha_qubits[idx1], alpha_qubits[idx2]) + assert dist == 1 + + dist = coupling_map.distance(beta_qubits[idx1], beta_qubits[idx2]) + assert dist == 1 From 275fcc5734468e3d38e23210c4071ea9fa14f314 Mon Sep 17 00:00:00 2001 From: ashsaki Date: Mon, 22 Sep 2025 19:14:09 -0400 Subject: [PATCH 4/5] fix ci errors --- .../lucj_heavy_hex_preset_pass_manager.py | 2 ++ .../lucj_heavy_hex_preset_pass_manager_test.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py index 2c7b3731f..b6f452e1c 100644 --- a/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py +++ b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy import warnings from typing import Any, Sequence diff --git a/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py b/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py index 7a439be3b..dafef05f4 100644 --- a/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py +++ b/tests/python/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager_test.py @@ -1,11 +1,18 @@ import pytest from qiskit import QuantumCircuit, QuantumRegister -from qiskit_ibm_runtime.fake_provider import FakeMarrakesh +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler import CouplingMap import ffsim from ffsim.qiskit import generate_pm_and_interactions_lucj_heavy_hex -backend = FakeMarrakesh() +cmap = CouplingMap.from_heavy_hex(distance=9) +backend = GenericBackendV2( + num_qubits=cmap.size(), + basis_gates=["id", "rz", "sx", "x", "cz"], + coupling_map=cmap, + noise_info=True, +) num_orbitals = 36 From a29fdf090ed6b853215e86dfb7747fea6a08d0e9 Mon Sep 17 00:00:00 2001 From: ashsaki Date: Wed, 1 Oct 2025 18:28:42 -0400 Subject: [PATCH 5/5] fix docstring error --- .../lucj_heavy_hex_preset_pass_manager.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py index b6f452e1c..4f87a84e6 100644 --- a/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py +++ b/python/ffsim/qiskit/transpiler_passes/lucj_heavy_hex_preset_pass_manager.py @@ -18,8 +18,10 @@ def _create_two_linear_chains(num_orbitals: int) -> PyGraph: - """In zig-zag layout, there are two linear chains (with connecting qubits between - the chains). This function creates those two linear chains which is a rustworkx + """In zig-zag layout, there are two linear chains (with connecting qubits + between the chains). This function creates those two linear chains which is + a rustworkx. + PyGraph with two disconnected linear chains. Each chain contains `num_orbitals` number of nodes, i.e., in the final graph there are `2 * num_orbitals` number of nodes. @@ -54,20 +56,19 @@ def _get_layout_graph_and_allowed_alpha_beta_indices( backend_coupling_graph: PyGraph, alpha_beta_indices: list[tuple[int, int]], ) -> tuple[PyGraph, list[tuple[int, int]]]: - """This function creates the complete zigzag graph that _can be mapped_ to a IBM - QPU with heavy-hex connectivity (i.e., the zigzag pattern is an isomorphic - sub-graph to the QPU/backend coupling graph). The zigzag pattern includes - both linear chains (alpha-alpha/beta-beta interactions) and connecting qubits - between the linear chains (alpha-beta interactions). - - The algorithm works as follows: It starts with an interm graph (`graph_new`) - that has two linear chains with connecting nodes between two nodes (qubits) - specified by `alpha_beta_indices` list. The algorithm checks if the starting - graph is an isomorphic subgraph to the larger `backend_cpupling_graph`. If yes, - the routine ends and returns the `graph_new`. If not, it removes an alpha-beta - interaction pair from the end of list `alpha_beta_indices` and checks for - subgraph isomorphism again. It cycle continues, until a isomorhic subgraph is - found. + """This function creates the complete zigzag graph that _can be mapped_ to + a IBM QPU with heavy-hex connectivity (i.e., the zigzag pattern is an + isomorphic sub-graph to the QPU/backend coupling graph). The zigzag pattern + includes both linear chains (alpha-alpha/beta-beta interactions) and + connecting qubits between the linear chains (alpha-beta interactions). + The algorithm works as follows: It starts with an interm graph (`graph_new`) + that has two linear chains with connecting nodes between two nodes (qubits) + specified by `alpha_beta_indices` list. The algorithm checks if the starting + graph is an isomorphic subgraph to the larger `backend_cpupling_graph`. If yes, + the routine ends and returns the `graph_new`. If not, it removes an alpha-beta + interaction pair from the end of list `alpha_beta_indices` and checks for + subgraph isomorphism again. It cycle continues, until a isomorhic subgraph is + found. Args: num_orbitals: Number of orbitals, i.e., number of nodes in each alpha-alpha @@ -127,8 +128,8 @@ def _get_layout_graph_and_allowed_alpha_beta_indices( def _make_backend_cmap_pygraph(backend: BackendV2) -> PyGraph: - """Converts an IBM backend coupling map to an undirected rustworkx.PyGraph where - there is only a single edge between same two nodes. + """Converts an IBM backend coupling map to an undirected rustworkx.PyGraph + where there is only a single edge between same two nodes. Args: backend: An IBM backend. @@ -160,8 +161,8 @@ def _get_placeholder_layout_and_allowed_interactions( num_orbitals: int, requested_alpha_beta_indices: Sequence[tuple[int, int]], ) -> tuple[list[int], list[tuple[int, int]]]: - """The main function that generates the zigzag pattern with physical qubits that - can be used as an `intial_layout` in a preset passmanager/transpiler. + """The main function that generates the zigzag pattern with physical qubits + that can be used as an `intial_layout` in a preset passmanager/transpiler. Args: num_orbitals: Number of orbitals. @@ -235,15 +236,15 @@ def generate_pm_and_interactions_lucj_heavy_hex( allowed by backend connectivity]. Default: `None`. **qiskit_pm_kwargs: The function accepts full list of arguments from - [`qiskit.transpiler.generate_preset_pass_manager`](https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.transpiler.generate_preset_pass_manager) + `qiskit.transpiler.generate_preset_pass_manager `_ except `initial_layout` and `layout_method` as they are conflicting with this routine's functionality. + If specified, they will be deleted with a warning. Returns: - pm: A preset pass manager. - allowed_alpha_beta_indices: A list of alpha-beta pairs that can be accomodated - on the backend. + - A preset pass manager. + - A list of alpha-beta pairs that can be accomodated on the backend. """ # noqa: E501 if "initial_layout" in qiskit_pm_kwargs: warnings.warn("Argument `initial_layout` is ignored.")