diff --git a/crates/accelerate/src/inverse_cancellation.rs b/crates/accelerate/src/inverse_cancellation.rs new file mode 100644 index 000000000000..b150297f4917 --- /dev/null +++ b/crates/accelerate/src/inverse_cancellation.rs @@ -0,0 +1,193 @@ +// 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. + +use ahash::RandomState; +use hashbrown::HashSet; +use indexmap::IndexMap; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::operations::Operation; +use qiskit_circuit::packed_instruction::PackedInstruction; + +fn gate_eq(py: Python, gate_a: &PackedInstruction, gate_b: &OperationFromPython) -> PyResult { + if gate_a.op.name() != gate_b.operation.name() { + return Ok(false); + } + let a_params = gate_a.params_view(); + if a_params.len() != gate_b.params.len() { + return Ok(false); + } + let mut param_eq = true; + for (a, b) in a_params.iter().zip(&gate_b.params) { + if !a.is_close(py, b, 1e-10)? { + param_eq = false; + break; + } + } + Ok(param_eq) +} + +fn run_on_self_inverse( + py: Python, + dag: &mut DAGCircuit, + op_counts: &IndexMap, + self_inverse_gate_names: HashSet, + self_inverse_gates: Vec, +) -> PyResult<()> { + if !self_inverse_gate_names + .iter() + .any(|name| op_counts.contains_key(name)) + { + return Ok(()); + } + for gate in self_inverse_gates { + let gate_count = op_counts.get(gate.operation.name()).unwrap_or(&0); + if *gate_count <= 1 { + continue; + } + let mut collect_set: HashSet = HashSet::with_capacity(1); + collect_set.insert(gate.operation.name().to_string()); + let gate_runs: Vec> = dag.collect_runs(collect_set).unwrap().collect(); + for gate_cancel_run in gate_runs { + let mut partitions: Vec> = Vec::new(); + let mut chunk: Vec = Vec::new(); + let max_index = gate_cancel_run.len() - 1; + for (i, cancel_gate) in gate_cancel_run.iter().enumerate() { + let node = &dag.dag[*cancel_gate]; + if let NodeType::Operation(inst) = node { + if gate_eq(py, inst, &gate)? { + chunk.push(*cancel_gate); + } else { + let is_empty: bool = chunk.is_empty(); + if !is_empty { + partitions.push(std::mem::take(&mut chunk)); + } + continue; + } + if i == max_index { + partitions.push(std::mem::take(&mut chunk)); + } else { + let next_qargs = if let NodeType::Operation(next_inst) = + &dag.dag[gate_cancel_run[i + 1]] + { + next_inst.qubits + } else { + panic!("Not an op node") + }; + if inst.qubits != next_qargs { + partitions.push(std::mem::take(&mut chunk)); + } + } + } else { + panic!("Not an op node"); + } + } + for chunk in partitions { + if chunk.len() % 2 == 0 { + dag.remove_op_node(chunk[0]); + } + for node in &chunk[1..] { + dag.remove_op_node(*node); + } + } + } + } + Ok(()) +} +fn run_on_inverse_pairs( + py: Python, + dag: &mut DAGCircuit, + op_counts: &IndexMap, + inverse_gate_names: HashSet, + inverse_gates: Vec<[OperationFromPython; 2]>, +) -> PyResult<()> { + if !inverse_gate_names + .iter() + .any(|name| op_counts.contains_key(name)) + { + return Ok(()); + } + for pair in inverse_gates { + let gate_0_name = pair[0].operation.name(); + let gate_1_name = pair[1].operation.name(); + if !op_counts.contains_key(gate_0_name) || !op_counts.contains_key(gate_1_name) { + continue; + } + let names: HashSet = pair + .iter() + .map(|x| x.operation.name().to_string()) + .collect(); + let runs: Vec> = dag.collect_runs(names).unwrap().collect(); + for nodes in runs { + let mut i = 0; + while i < nodes.len() - 1 { + if let NodeType::Operation(inst) = &dag.dag[nodes[i]] { + if let NodeType::Operation(next_inst) = &dag.dag[nodes[i + 1]] { + if inst.qubits == next_inst.qubits + && ((gate_eq(py, inst, &pair[0])? && gate_eq(py, next_inst, &pair[1])?) + || (gate_eq(py, inst, &pair[1])? + && gate_eq(py, next_inst, &pair[0])?)) + { + dag.remove_op_node(nodes[i]); + dag.remove_op_node(nodes[i + 1]); + i += 2; + } else { + i += 1; + } + } else { + panic!("Not an op node") + } + } else { + panic!("Not an op node") + } + } + } + } + Ok(()) +} + +#[pyfunction] +pub fn inverse_cancellation( + py: Python, + dag: &mut DAGCircuit, + inverse_gates: Vec<[OperationFromPython; 2]>, + self_inverse_gates: Vec, + inverse_gate_names: HashSet, + self_inverse_gate_names: HashSet, +) -> PyResult<()> { + let op_counts = if !self_inverse_gate_names.is_empty() || !inverse_gate_names.is_empty() { + dag.count_ops(py, true)? + } else { + IndexMap::default() + }; + if !self_inverse_gate_names.is_empty() { + run_on_self_inverse( + py, + dag, + &op_counts, + self_inverse_gate_names, + self_inverse_gates, + )?; + } + if !inverse_gate_names.is_empty() { + run_on_inverse_pairs(py, dag, &op_counts, inverse_gate_names, inverse_gates)?; + } + Ok(()) +} + +pub fn inverse_cancellation_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(inverse_cancellation))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 5414183d22cc..0aba530ab91a 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -20,6 +20,7 @@ pub mod dense_layout; pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; +pub mod inverse_cancellation; pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index afe6797596d9..9174fcf93e88 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -4570,45 +4570,9 @@ def _format(operand): /// /// Returns: /// Mapping[str, int]: a mapping of operation names to the number of times it appears. - #[pyo3(signature = (*, recurse=true))] - fn count_ops(&self, py: Python, recurse: bool) -> PyResult { - if !recurse || !self.has_control_flow() { - Ok(self.op_names.to_object(py)) - } else { - fn inner( - py: Python, - dag: &DAGCircuit, - counts: &mut HashMap, - ) -> PyResult<()> { - for (key, value) in dag.op_names.iter() { - counts - .entry(key.clone()) - .and_modify(|count| *count += value) - .or_insert(*value); - } - let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); - for node in dag.dag.node_weights() { - let NodeType::Operation(node) = node else { - continue; - }; - if !node.op.control_flow() { - continue; - } - let OperationRef::Instruction(inst) = node.op.view() else { - panic!("control flow op must be an instruction") - }; - let blocks = inst.instruction.bind(py).getattr("blocks")?; - for block in blocks.iter()? { - let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; - inner(py, inner_dag, counts)?; - } - } - Ok(()) - } - let mut counts = HashMap::with_capacity(self.op_names.len()); - inner(py, self, &mut counts)?; - Ok(counts.to_object(py)) - } + #[pyo3(name = "count_ops", signature = (*, recurse=true))] + fn py_count_ops(&self, py: Python, recurse: bool) -> PyResult { + self.count_ops(py, recurse).map(|x| x.into_py(py)) } /// Count the occurrences of operation names on the longest path. @@ -4740,7 +4704,7 @@ def _format(operand): ("qubits", self.num_qubits().into_py(py)), ("bits", self.num_clbits().into_py(py)), ("factors", self.num_tensor_factors().into_py(py)), - ("operations", self.count_ops(py, true)?), + ("operations", self.py_count_ops(py, true)?), ])) } @@ -6277,6 +6241,51 @@ impl DAGCircuit { Err(DAGCircuitError::new_err("Specified node is not an op node")) } } + + pub fn count_ops( + &self, + py: Python, + recurse: bool, + ) -> PyResult> { + if !recurse || !self.has_control_flow() { + Ok(self.op_names.clone()) + } else { + fn inner( + py: Python, + dag: &DAGCircuit, + counts: &mut IndexMap, + ) -> PyResult<()> { + for (key, value) in dag.op_names.iter() { + counts + .entry(key.clone()) + .and_modify(|count| *count += value) + .or_insert(*value); + } + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + for node in dag.dag.node_weights() { + let NodeType::Operation(node) = node else { + continue; + }; + if !node.op.control_flow() { + continue; + } + let OperationRef::Instruction(inst) = node.op.view() else { + panic!("control flow op must be an instruction") + }; + let blocks = inst.instruction.bind(py).getattr("blocks")?; + for block in blocks.iter()? { + let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; + inner(py, inner_dag, counts)?; + } + } + Ok(()) + } + let mut counts = + IndexMap::with_capacity_and_hasher(self.op_names.len(), RandomState::default()); + inner(py, self, &mut counts)?; + Ok(counts) + } + } } /// Add to global phase. Global phase can only be Float or ParameterExpression so this diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index e8971bc87629..8d201b6170c3 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -15,7 +15,8 @@ use pyo3::prelude::*; use qiskit_accelerate::{ circuit_library::circuit_library, convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, error_map::error_map, - euler_one_qubit_decomposer::euler_one_qubit_decomposer, isometry::isometry, nlayout::nlayout, + euler_one_qubit_decomposer::euler_one_qubit_decomposer, + inverse_cancellation::inverse_cancellation_mod, 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, star_prerouting::star_prerouting, stochastic_swap::stochastic_swap, synthesis::synthesis, @@ -44,6 +45,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, dense_layout, "dense_layout")?; add_submodule(m, error_map, "error_map")?; add_submodule(m, euler_one_qubit_decomposer, "euler_one_qubit_decomposer")?; + add_submodule(m, inverse_cancellation_mod, "inverse_cancellation")?; add_submodule(m, isometry, "isometry")?; add_submodule(m, nlayout, "nlayout")?; add_submodule(m, optimize_1q_gates, "optimize_1q_gates")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 33933fd8fd7e..700b02e02a16 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -87,6 +87,7 @@ sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear sys.modules["qiskit._accelerate.synthesis.clifford"] = _accelerate.synthesis.clifford sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py index f5523432c26e..40876679e8d9 100644 --- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py @@ -20,6 +20,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit._accelerate.inverse_cancellation import inverse_cancellation + class InverseCancellation(TransformationPass): """Cancel specific Gates which are inverses of each other when they occur back-to- @@ -81,96 +83,11 @@ def run(self, dag: DAGCircuit): Returns: DAGCircuit: Transformed DAG. """ - if self.self_inverse_gates: - dag = self._run_on_self_inverse(dag) - if self.inverse_gate_pairs: - dag = self._run_on_inverse_pairs(dag) - return dag - - def _run_on_self_inverse(self, dag: DAGCircuit): - """ - Run self-inverse gates on `dag`. - - Args: - dag: the directed acyclic graph to run on. - self_inverse_gates: list of gates who cancel themeselves in pairs - - Returns: - DAGCircuit: Transformed DAG. - """ - op_counts = dag.count_ops() - if not self.self_inverse_gate_names.intersection(op_counts): - return dag - # Sets of gate runs by name, for instance: [{(H 0, H 0), (H 1, H 1)}, {(X 0, X 0}] - for gate in self.self_inverse_gates: - gate_name = gate.name - gate_count = op_counts.get(gate_name, 0) - if gate_count <= 1: - continue - gate_runs = dag.collect_runs([gate_name]) - for gate_cancel_run in gate_runs: - partitions = [] - chunk = [] - max_index = len(gate_cancel_run) - 1 - for i, cancel_gate in enumerate(gate_cancel_run): - if cancel_gate.op == gate: - chunk.append(cancel_gate) - else: - if chunk: - partitions.append(chunk) - chunk = [] - continue - if i == max_index or cancel_gate.qargs != gate_cancel_run[i + 1].qargs: - partitions.append(chunk) - chunk = [] - # Remove an even number of gates from each chunk - for chunk in partitions: - if len(chunk) % 2 == 0: - dag.remove_op_node(chunk[0]) - for node in chunk[1:]: - dag.remove_op_node(node) - return dag - - def _run_on_inverse_pairs(self, dag: DAGCircuit): - """ - Run inverse gate pairs on `dag`. - - Args: - dag: the directed acyclic graph to run on. - inverse_gate_pairs: list of gates with inverse angles that cancel each other. - - Returns: - DAGCircuit: Transformed DAG. - """ - op_counts = dag.count_ops() - if not self.inverse_gate_pairs_names.intersection(op_counts): - return dag - - for pair in self.inverse_gate_pairs: - gate_0_name = pair[0].name - gate_1_name = pair[1].name - if gate_0_name not in op_counts or gate_1_name not in op_counts: - continue - gate_cancel_runs = dag.collect_runs([gate_0_name, gate_1_name]) - for dag_nodes in gate_cancel_runs: - i = 0 - while i < len(dag_nodes) - 1: - if ( - dag_nodes[i].qargs == dag_nodes[i + 1].qargs - and dag_nodes[i].op == pair[0] - and dag_nodes[i + 1].op == pair[1] - ): - dag.remove_op_node(dag_nodes[i]) - dag.remove_op_node(dag_nodes[i + 1]) - i = i + 2 - elif ( - dag_nodes[i].qargs == dag_nodes[i + 1].qargs - and dag_nodes[i].op == pair[1] - and dag_nodes[i + 1].op == pair[0] - ): - dag.remove_op_node(dag_nodes[i]) - dag.remove_op_node(dag_nodes[i + 1]) - i = i + 2 - else: - i = i + 1 + inverse_cancellation( + dag, + self.inverse_gate_pairs, + self.self_inverse_gates, + self.inverse_gate_pairs_names, + self.self_inverse_gate_names, + ) return dag