diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 314fa5ff7c58..4e079ea84b57 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -27,6 +27,7 @@ pub mod results; pub mod sabre; pub mod sampled_exp_val; pub mod sparse_pauli_op; +pub mod star_prerouting; pub mod stochastic_swap; pub mod synthesis; pub mod target_transpiler; diff --git a/crates/accelerate/src/sabre/mod.rs b/crates/accelerate/src/sabre/mod.rs index 3eb8ebb3a219..1229be16b723 100644 --- a/crates/accelerate/src/sabre/mod.rs +++ b/crates/accelerate/src/sabre/mod.rs @@ -14,8 +14,8 @@ mod layer; mod layout; mod neighbor_table; mod route; -mod sabre_dag; -mod swap_map; +pub mod sabre_dag; +pub mod swap_map; use hashbrown::HashMap; use numpy::{IntoPyArray, ToPyArray}; diff --git a/crates/accelerate/src/star_prerouting.rs b/crates/accelerate/src/star_prerouting.rs new file mode 100644 index 000000000000..fd2156ad2011 --- /dev/null +++ b/crates/accelerate/src/star_prerouting.rs @@ -0,0 +1,214 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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. + +/// Type alias for a node representation. +/// Each node is represented as a tuple containing: +/// - Node id (usize) +/// - List of involved qubit indices (Vec) +/// - Set of involved classical bit indices (HashSet) +/// - Directive flag (bool) +type Nodes = (usize, Vec, HashSet, bool); + +/// Type alias for a block representation. +/// Each block is represented by a tuple containing: +/// - A boolean indicating the presence of a center (bool) +/// - A list of nodes (Vec) +type Block = (bool, Vec); + +use crate::nlayout::PhysicalQubit; +use crate::nlayout::VirtualQubit; +use crate::sabre::sabre_dag::SabreDAG; +use crate::sabre::swap_map::SwapMap; +use crate::sabre::BlockResult; +use crate::sabre::NodeBlockResults; +use crate::sabre::SabreResult; +use hashbrown::HashMap; +use hashbrown::HashSet; +use numpy::IntoPyArray; +use pyo3::prelude::*; + +/// Python function to perform star prerouting on a SabreDAG. +/// This function processes star blocks and updates the DAG and qubit mapping. +#[pyfunction] +#[pyo3(text_signature = "(dag, blocks, processing_order, /)")] +fn star_preroute( + py: Python, + dag: &mut SabreDAG, + blocks: Vec, + processing_order: Vec, +) -> (SwapMap, PyObject, NodeBlockResults, PyObject) { + let mut qubit_mapping: Vec = (0..dag.num_qubits).collect(); + let mut processed_block_ids: HashSet = HashSet::with_capacity(blocks.len()); + let last_2q_gate = processing_order.iter().rev().find(|node| node.1.len() == 2); + let mut is_first_star = true; + + // Structures for SabreResult + let mut out_map: HashMap> = + HashMap::with_capacity(dag.dag.node_count()); + let mut gate_order: Vec = Vec::with_capacity(dag.dag.node_count()); + let node_block_results: HashMap> = HashMap::new(); + + // Create a HashMap to store the node-to-block mapping + let mut node_to_block: HashMap = HashMap::with_capacity(processing_order.len()); + for (block_id, block) in blocks.iter().enumerate() { + for node in &block.1 { + node_to_block.insert(node.0, block_id); + } + } + // Store nodes where swaps will be placed. + let mut swap_locations: Vec<&Nodes> = Vec::with_capacity(processing_order.len()); + + // Process blocks, gathering swap locations and updating the gate order + for node in &processing_order { + if let Some(&block_id) = node_to_block.get(&node.0) { + // Skip if the block has already been processed + if !processed_block_ids.insert(block_id) { + continue; + } + process_block( + &blocks[block_id], + last_2q_gate, + &mut is_first_star, + &mut gate_order, + &mut swap_locations, + ); + } else { + // Apply operation for nodes not part of any block + gate_order.push(node.0); + } + } + + // Apply the swaps based on the gathered swap locations and gate order + for (index, node_id) in gate_order.iter().enumerate() { + for swap_location in &swap_locations { + if *node_id == swap_location.0 { + if let Some(next_node_id) = gate_order.get(index + 1) { + apply_swap( + &mut qubit_mapping, + &swap_location.1, + *next_node_id, + &mut out_map, + ); + } + } + } + } + + let res = SabreResult { + map: SwapMap { map: out_map }, + node_order: gate_order, + node_block_results: NodeBlockResults { + results: node_block_results, + }, + }; + + let final_res = ( + res.map, + res.node_order.into_pyarray_bound(py).into(), + res.node_block_results, + qubit_mapping.into_pyarray_bound(py).into(), + ); + + final_res +} + +/// Processes a star block, applying operations and handling swaps. +/// +/// Args: +/// +/// * `block` - A tuple containing a boolean indicating the presence of a center and a vector of nodes representing the star block. +/// * `last_2q_gate` - The last two-qubit gate in the processing order. +/// * `is_first_star` - A mutable reference to a boolean indicating if this is the first star block being processed. +/// * `gate_order` - A mutable reference to the gate order vector. +/// * `swap_locations` - A mutable reference to the nodes where swaps will be placed after +fn process_block<'a>( + block: &'a Block, + last_2q_gate: Option<&'a Nodes>, + is_first_star: &mut bool, + gate_order: &mut Vec, + swap_locations: &mut Vec<&'a Nodes>, +) { + let (has_center, sequence) = block; + + // If the block contains exactly 2 nodes, apply them directly + if sequence.len() == 2 { + for inner_node in sequence { + gate_order.push(inner_node.0); + } + return; + } + + let mut prev_qargs = None; + let mut swap_source = false; + + // Process each node in the block + for inner_node in sequence.iter() { + // Apply operation directly if it's a single-qubit operation or the same as previous qargs + if inner_node.1.len() == 1 || prev_qargs == Some(&inner_node.1) { + gate_order.push(inner_node.0); + continue; + } + + // If this is the first star and no swap source has been identified, set swap_source + if *is_first_star && !swap_source { + swap_source = *has_center; + gate_order.push(inner_node.0); + prev_qargs = Some(&inner_node.1); + continue; + } + + // Place 2q-gate and subsequent swap gate + gate_order.push(inner_node.0); + + if inner_node != last_2q_gate.unwrap() && inner_node.1.len() == 2 { + swap_locations.push(inner_node); + } + prev_qargs = Some(&inner_node.1); + } + *is_first_star = false; +} + +/// Applies a swap operation to the DAG and updates the qubit mapping. +/// +/// # Args: +/// +/// * `qubit_mapping` - A mutable reference to the qubit mapping vector. +/// * `qargs` - Qubit indices for the swap operation (node before the swap) +/// * `next_node_id` - ID of the next node in the gate order (node after the swap) +/// * `out_map` - A mutable reference to the output map. +fn apply_swap( + qubit_mapping: &mut [usize], + qargs: &[VirtualQubit], + next_node_id: usize, + out_map: &mut HashMap>, +) { + if qargs.len() == 2 { + let idx0 = qargs[0].index(); + let idx1 = qargs[1].index(); + + // Update the `qubit_mapping` and `out_map` to reflect the swap operation + qubit_mapping.swap(idx0, idx1); + out_map.insert( + next_node_id, + vec![[ + PhysicalQubit::new(qubit_mapping[idx0].try_into().unwrap()), + PhysicalQubit::new(qubit_mapping[idx1].try_into().unwrap()), + ]], + ); + } +} + +#[pymodule] +pub fn star_prerouting(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(star_preroute))?; + Ok(()) +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 6af99ff04a8d..f9711641a938 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -18,9 +18,10 @@ use qiskit_accelerate::{ error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, - sparse_pauli_op::sparse_pauli_op, stochastic_swap::stochastic_swap, synthesis::synthesis, - target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, - utils::utils, vf2_layout::vf2_layout, + sparse_pauli_op::sparse_pauli_op, star_prerouting::star_prerouting, + stochastic_swap::stochastic_swap, synthesis::synthesis, target_transpiler::target, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, + vf2_layout::vf2_layout, }; #[pymodule] @@ -41,6 +42,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(sabre))?; m.add_wrapped(wrap_pymodule!(sampled_exp_val))?; m.add_wrapped(wrap_pymodule!(sparse_pauli_op))?; + m.add_wrapped(wrap_pymodule!(star_prerouting))?; m.add_wrapped(wrap_pymodule!(stochastic_swap))?; m.add_wrapped(wrap_pymodule!(target))?; m.add_wrapped(wrap_pymodule!(two_qubit_decompose))?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index d88261cad209..6091bfa90346 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -77,6 +77,7 @@ sys.modules["qiskit._accelerate.sabre"] = _accelerate.sabre sys.modules["qiskit._accelerate.sampled_exp_val"] = _accelerate.sampled_exp_val sys.modules["qiskit._accelerate.sparse_pauli_op"] = _accelerate.sparse_pauli_op +sys.modules["qiskit._accelerate.star_prerouting"] = _accelerate.star_prerouting sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap sys.modules["qiskit._accelerate.target"] = _accelerate.target sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index c00cee74de4b..53bc971a268b 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -14,11 +14,15 @@ from typing import Iterable, Union, Optional, List, Tuple from math import floor, log10 -from qiskit.circuit import Barrier -from qiskit.circuit.library import SwapGate +from qiskit.circuit import SwitchCaseOp, Clbit, ClassicalRegister, Barrier +from qiskit.circuit.controlflow import condition_resources, node_resources from qiskit.dagcircuit import DAGOpNode, DAGDepNode, DAGDependency, DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.layout import Layout +from qiskit.transpiler.passes.routing.sabre_swap import _build_sabre_dag, _apply_sabre_result + +from qiskit._accelerate import star_prerouting +from qiskit._accelerate.nlayout import NLayout class StarBlock: @@ -305,113 +309,84 @@ def star_preroute(self, dag, blocks, processing_order): new_dag: a dag specifying the pre-routed circuit qubit_mapping: the final qubit mapping after pre-routing """ - node_to_block_id = {} - for i, block in enumerate(blocks): - for node in block.get_nodes(): - node_to_block_id[node] = i - - new_dag = dag.copy_empty_like() - processed_block_ids = set() - qubit_mapping = list(range(len(dag.qubits))) - - def _apply_mapping(qargs, qubit_mapping, qubits): - return tuple(qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs) - - is_first_star = True - last_2q_gate = [ - op - for op in reversed(processing_order) - if ((len(op.qargs) > 1) and (op.name != "barrier")) + # Convert the DAG to a SabreDAG + num_qubits = len(dag.qubits) + canonical_register = dag.qregs["q"] + current_layout = Layout.generate_trivial_layout(canonical_register) + qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + layout_mapping = {qubit_indices[k]: v for k, v in current_layout.get_virtual_bits().items()} + initial_layout = NLayout(layout_mapping, num_qubits, num_qubits) + sabre_dag, circuit_to_dag_dict = _build_sabre_dag(dag, num_qubits, qubit_indices) + + # Extract the nodes from the blocks for the Rust representation + rust_blocks = [ + (block.center is not None, _extract_nodes(block.get_nodes(), dag)) for block in blocks ] - if len(last_2q_gate) > 0: - last_2q_gate = last_2q_gate[0] - else: - last_2q_gate = None + # Determine the processing order of the nodes in the DAG for the Rust representation int_digits = floor(log10(len(processing_order))) + 1 processing_order_index_map = { - node: f"a{str(index).zfill(int(int_digits))}" - for index, node in enumerate(processing_order) + node: f"a{index:0{int_digits}}" for index, node in enumerate(processing_order) } def tie_breaker_key(node): return processing_order_index_map.get(node, node.sort_key) - for node in dag.topological_op_nodes(key=tie_breaker_key): - block_id = node_to_block_id.get(node, None) - if block_id is not None: - if block_id in processed_block_ids: - continue - - processed_block_ids.add(block_id) - - # process the whole block - block = blocks[block_id] - sequence = block.nodes - center_node = block.center - - if len(sequence) == 2: - for inner_node in sequence: - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - continue - swap_source = None - prev = None - for inner_node in sequence: - if (len(inner_node.qargs) == 1) or (inner_node.qargs == prev): - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - continue - if is_first_star and swap_source is None: - swap_source = center_node - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - - prev = inner_node.qargs - continue - # place 2q-gate and subsequent swap gate - new_dag.apply_operation_back( - inner_node.op, - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - - if not inner_node is last_2q_gate and not isinstance(inner_node.op, Barrier): - new_dag.apply_operation_back( - SwapGate(), - _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), - inner_node.cargs, - check=False, - ) - # Swap mapping - index_0 = dag.find_bit(inner_node.qargs[0]).index - index_1 = dag.find_bit(inner_node.qargs[1]).index - qubit_mapping[index_1], qubit_mapping[index_0] = ( - qubit_mapping[index_0], - qubit_mapping[index_1], - ) - - prev = inner_node.qargs - is_first_star = False - else: - # the node is not part of a block - new_dag.apply_operation_back( - node.op, - _apply_mapping(node.qargs, qubit_mapping, dag.qubits), - node.cargs, - check=False, - ) - return new_dag, qubit_mapping + rust_processing_order = _extract_nodes(dag.topological_op_nodes(key=tie_breaker_key), dag) + + # Run the star prerouting algorithm to obtain the new DAG and qubit mapping + *sabre_result, qubit_mapping = star_prerouting.star_preroute( + sabre_dag, rust_blocks, rust_processing_order + ) + + res_dag = _apply_sabre_result( + dag.copy_empty_like(), + dag, + sabre_result, + initial_layout, + dag.qubits, + circuit_to_dag_dict, + ) + + return res_dag, qubit_mapping + + +def _extract_nodes(nodes, dag): + """Extract and format node information for Rust representation used in SabreDAG. + + Each node is represented as a tuple containing: + - Node ID (int): The unique identifier of the node in the DAG. + - Qubit indices (list of int): Indices of qubits involved in the node's operation. + - Classical bit indices (set of int): Indices of classical bits involved in the node's operation. + - Directive flag (bool): Indicates whether the operation is a directive (True) or not (False). + + Args: + nodes (list[DAGOpNode]): List of DAGOpNode objects to extract information from. + dag (DAGCircuit): DAGCircuit object containing the circuit structure. + + Returns: + list of tuples: Each tuple contains information about a node in the format described above. + """ + extracted_node_info = [] + for node in nodes: + qubit_indices = [dag.find_bit(qubit).index for qubit in node.qargs] + classical_bit_indices = set() + + if node.op.condition is not None: + classical_bit_indices.update(condition_resources(node.op.condition).clbits) + + if isinstance(node.op, SwitchCaseOp): + switch_case_target = node.op.target + if isinstance(switch_case_target, Clbit): + classical_bit_indices.add(switch_case_target) + elif isinstance(switch_case_target, ClassicalRegister): + classical_bit_indices.update(switch_case_target) + else: # Assume target is an expression involving classical bits + classical_bit_indices.update(node_resources(switch_case_target).clbits) + + is_directive = getattr(node.op, "_directive", False) + extracted_node_info.append( + (node._node_id, qubit_indices, classical_bit_indices, is_directive) + ) + + return extracted_node_info diff --git a/releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml b/releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml new file mode 100644 index 000000000000..f8eca807bec6 --- /dev/null +++ b/releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml @@ -0,0 +1,11 @@ +--- +features_transpiler: + - | + Port part of the logic from the :class:`StarPrerouting`, used to + find a star graph connectivity subcircuit and replaces it with a + linear routing equivalent. + - | + The function :func:`star_preroute` now performs the heavily lifting + to transform the dag by in the rust space by taking advantage + of the functions :func:`_build_sabre_dag` and + :func:`_apply_sabre_result`. diff --git a/test/python/transpiler/test_star_prerouting.py b/test/python/transpiler/test_star_prerouting.py index fb67698300b4..2744113f13cb 100644 --- a/test/python/transpiler/test_star_prerouting.py +++ b/test/python/transpiler/test_star_prerouting.py @@ -24,6 +24,7 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.quantum_info import Operator from qiskit.transpiler.passes import VF2Layout, ApplyLayout, SabreSwap, SabreLayout +from qiskit.transpiler.passes.layout.vf2_utils import build_interaction_graph from qiskit.transpiler.passes.routing.star_prerouting import StarPreRouting from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.passmanager import PassManager @@ -480,3 +481,23 @@ def test_routing_after_star_prerouting(self): self.assertTrue(Operator.from_circuit(res_sabre), qc) self.assertTrue(Operator.from_circuit(res_star), qc) self.assertTrue(Operator.from_circuit(res_star), Operator.from_circuit(res_sabre)) + + @ddt.data(4, 8, 16, 32) + def test_qft_linearization(self, num_qubits): + """Test the QFT circuit to verify if it is linearized and requires n-2 swaps.""" + + qc = QFT(num_qubits, do_swaps=False, insert_barriers=True).decompose() + dag = circuit_to_dag(qc) + new_dag = StarPreRouting().run(dag) + new_qc = dag_to_circuit(new_dag) + + # Check that resulting result has n-2 swaps, where n is the number of cp gates + swap_count = new_qc.count_ops().get("swap", 0) + cp_count = new_qc.count_ops().get("cp", 0) + self.assertEqual(swap_count, cp_count - 2) + + # Confirm linearization by checking that the number of edges is equal to the number of nodes + interaction_graph = build_interaction_graph(new_dag, strict_direction=False)[0] + num_edges = interaction_graph.num_edges() + num_nodes = interaction_graph.num_nodes() + self.assertEqual(num_edges, num_nodes - 1)