-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fully port InverseCancellation to Rust #13013
Changes from all commits
170148c
563eb08
efeeda5
00153dd
7dee702
3e6ebc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
// 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<bool> { | ||
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<String, usize, RandomState>, | ||
self_inverse_gate_names: HashSet<String>, | ||
self_inverse_gates: Vec<OperationFromPython>, | ||
) -> 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<String> = HashSet::with_capacity(1); | ||
collect_set.insert(gate.operation.name().to_string()); | ||
let gate_runs: Vec<Vec<NodeIndex>> = dag.collect_runs(collect_set).unwrap().collect(); | ||
for gate_cancel_run in gate_runs { | ||
let mut partitions: Vec<Vec<NodeIndex>> = Vec::new(); | ||
let mut chunk: Vec<NodeIndex> = 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 { | ||
if !chunk.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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool! I think this is the first example I've seen of the intern IDs saving us some time :) |
||
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<String, usize, RandomState>, | ||
inverse_gate_names: HashSet<String>, | ||
inverse_gates: Vec<[OperationFromPython; 2]>, | ||
) -> PyResult<()> { | ||
if !inverse_gate_names | ||
.iter() | ||
.any(|name| op_counts.contains_key(name)) | ||
{ | ||
return Ok(()); | ||
} | ||
for [gate_0, gate_1] in inverse_gates { | ||
let gate_0_name = gate_0.operation.name(); | ||
let gate_1_name = gate_1.operation.name(); | ||
if !op_counts.contains_key(gate_0_name) || !op_counts.contains_key(gate_1_name) { | ||
continue; | ||
} | ||
let names: HashSet<String> = [&gate_0, &gate_1] | ||
.iter() | ||
.map(|x| x.operation.name().to_string()) | ||
.collect(); | ||
let runs: Vec<Vec<NodeIndex>> = 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, &gate_0)? && gate_eq(py, next_inst, &gate_1)?) | ||
|| (gate_eq(py, inst, &gate_1)? | ||
&& gate_eq(py, next_inst, &gate_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<OperationFromPython>, | ||
inverse_gate_names: HashSet<String>, | ||
self_inverse_gate_names: HashSet<String>, | ||
) -> PyResult<()> { | ||
if self_inverse_gate_names.is_empty() && inverse_gate_names.is_empty() { | ||
return Ok(()); | ||
} | ||
let op_counts = dag.count_ops(py, true)?; | ||
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<PyModule>) -> PyResult<()> { | ||
m.add_wrapped(wrap_pyfunction!(inverse_cancellation))?; | ||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4575,45 +4575,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<PyObject> { | ||
if !recurse || !self.has_control_flow() { | ||
Ok(self.op_names.to_object(py)) | ||
} else { | ||
fn inner( | ||
py: Python, | ||
dag: &DAGCircuit, | ||
counts: &mut HashMap<String, usize>, | ||
) -> 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<PyObject> { | ||
self.count_ops(py, recurse).map(|x| x.into_py(py)) | ||
} | ||
|
||
/// Count the occurrences of operation names on the longest path. | ||
|
@@ -4745,7 +4709,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)?), | ||
])) | ||
} | ||
|
||
|
@@ -6365,6 +6329,56 @@ impl DAGCircuit { | |
} | ||
} | ||
|
||
/// Return the op name counts in the circuit | ||
/// | ||
/// Args: | ||
/// py: The python token necessary for control flow recursion | ||
/// recurse: Whether to recurse into control flow ops or not | ||
pub fn count_ops( | ||
&self, | ||
py: Python, | ||
recurse: bool, | ||
) -> PyResult<IndexMap<String, usize, RandomState>> { | ||
if !recurse || !self.has_control_flow() { | ||
Ok(self.op_names.clone()) | ||
} else { | ||
fn inner( | ||
py: Python, | ||
dag: &DAGCircuit, | ||
counts: &mut IndexMap<String, usize, RandomState>, | ||
) -> 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's worth adding a I'm not saying that we should necessarily bother maintaining op counts in Not necessary for this PR, but I wouldn't be opposed to doing it here either. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's funny you mention this, that was added last week: 5fc1635 but the implementation there is governed by the api of the python space method and not how I would implement it for performance in the transpiler. |
||
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) | ||
} | ||
} | ||
|
||
/// Extends the DAG with valid instances of [PackedInstruction] | ||
pub fn extend<I>(&mut self, py: Python, iter: I) -> PyResult<Vec<NodeIndex>> | ||
where | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like
DAGCircuit::collect_runs
returns anOption
only because it directly returns the result of the Rustworkx core function, which usesNone
to represent a cycle in the graph (...which is questionable lol).Perhaps it is beyond the scope of this PR, but it might be nice if you can change
DAGCircuit::collect_runs
to unwrap that option internally. If it fails, it should be onDAGCircuit
for getting itself into an invalid state.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm fine with making this change to
collect_runs()
but I'd say lets save this for a follow-up PR so we can do it in isolation.