Skip to content

Commit 458c77c

Browse files
alexanderivriikevinhartman
authored andcommitted
Port ElidePermutations transpiler pass to Rust (Qiskit#13094)
* initial commit * Rust docstring improvements * lint * restoring elided comment * explicitlt setting dtype=int for permutation gates * another attempt * fmt after merge * update after merge + comment from code review * switching to apply_operation_back * Comments from code review * fix using params --------- Co-authored-by: Kevin Hartman <kevin@hart.mn>
1 parent 1a31540 commit 458c77c

7 files changed

Lines changed: 127 additions & 33 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2024
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use numpy::PyReadonlyArray1;
14+
use pyo3::prelude::*;
15+
16+
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType};
17+
use qiskit_circuit::operations::{Operation, Param};
18+
use qiskit_circuit::Qubit;
19+
20+
/// Run the ElidePermutations pass on `dag`.
21+
/// Args:
22+
/// dag (DAGCircuit): the DAG to be optimized.
23+
/// Returns:
24+
/// An `Option`: the value of `None` indicates that no optimization was
25+
/// performed and the original `dag` should be used, otherwise it's a
26+
/// tuple consisting of the optimized DAG and the induced qubit permutation.
27+
#[pyfunction]
28+
fn run(py: Python, dag: &mut DAGCircuit) -> PyResult<Option<(DAGCircuit, Vec<usize>)>> {
29+
let permutation_gate_names = ["swap".to_string(), "permutation".to_string()];
30+
let op_counts = dag.count_ops(py, false)?;
31+
if !permutation_gate_names
32+
.iter()
33+
.any(|name| op_counts.contains_key(name))
34+
{
35+
return Ok(None);
36+
}
37+
let mut mapping: Vec<usize> = (0..dag.num_qubits()).collect();
38+
39+
// note that DAGCircuit::copy_empty_like clones the interners
40+
let mut new_dag = dag.copy_empty_like(py, "alike")?;
41+
for node_index in dag.topological_op_nodes()? {
42+
if let NodeType::Operation(inst) = &dag.dag()[node_index] {
43+
match (inst.op.name(), inst.condition()) {
44+
("swap", None) => {
45+
let qargs = dag.get_qargs(inst.qubits);
46+
let index0 = qargs[0].0 as usize;
47+
let index1 = qargs[1].0 as usize;
48+
mapping.swap(index0, index1);
49+
}
50+
("permutation", None) => {
51+
if let Param::Obj(ref pyobj) = inst.params.as_ref().unwrap()[0] {
52+
let pyarray: PyReadonlyArray1<i32> = pyobj.extract(py)?;
53+
let pattern = pyarray.as_array();
54+
55+
let qindices: Vec<usize> = dag
56+
.get_qargs(inst.qubits)
57+
.iter()
58+
.map(|q| q.0 as usize)
59+
.collect();
60+
61+
let remapped_qindices: Vec<usize> = (0..qindices.len())
62+
.map(|i| pattern[i])
63+
.map(|i| qindices[i as usize])
64+
.collect();
65+
66+
qindices
67+
.iter()
68+
.zip(remapped_qindices.iter())
69+
.for_each(|(old, new)| {
70+
mapping[*old] = *new;
71+
});
72+
} else {
73+
unreachable!();
74+
}
75+
}
76+
_ => {
77+
// General instruction
78+
let qargs = dag.get_qargs(inst.qubits);
79+
let cargs = dag.get_cargs(inst.clbits);
80+
let mapped_qargs: Vec<Qubit> = qargs
81+
.iter()
82+
.map(|q| q.0 as usize)
83+
.map(|q| mapping[q])
84+
.map(|q| Qubit(q.try_into().unwrap()))
85+
.collect();
86+
87+
new_dag.apply_operation_back(
88+
py,
89+
inst.op.clone(),
90+
&mapped_qargs,
91+
cargs,
92+
inst.params.as_deref().cloned(),
93+
inst.extra_attrs.clone(),
94+
#[cfg(feature = "cache_pygates")]
95+
None,
96+
)?;
97+
}
98+
}
99+
} else {
100+
unreachable!();
101+
}
102+
}
103+
Ok(Some((new_dag, mapping)))
104+
}
105+
106+
pub fn elide_permutations(m: &Bound<PyModule>) -> PyResult<()> {
107+
m.add_wrapped(wrap_pyfunction!(run))?;
108+
Ok(())
109+
}

crates/accelerate/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub mod commutation_checker;
2424
pub mod convert_2q_block_matrix;
2525
pub mod dense_layout;
2626
pub mod edge_collections;
27+
pub mod elide_permutations;
2728
pub mod equivalence;
2829
pub mod error_map;
2930
pub mod euler_one_qubit_decomposer;

crates/pyext/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
3939
add_submodule(m, ::qiskit_accelerate::dense_layout::dense_layout, "dense_layout")?;
4040
add_submodule(m, ::qiskit_accelerate::equivalence::equivalence, "equivalence")?;
4141
add_submodule(m, ::qiskit_accelerate::error_map::error_map, "error_map")?;
42+
add_submodule(m, ::qiskit_accelerate::elide_permutations::elide_permutations, "elide_permutations")?;
4243
add_submodule(m, ::qiskit_accelerate::euler_one_qubit_decomposer::euler_one_qubit_decomposer, "euler_one_qubit_decomposer")?;
4344
add_submodule(m, ::qiskit_accelerate::filter_op_nodes::filter_op_nodes_mod, "filter_op_nodes")?;
4445
add_submodule(m, ::qiskit_accelerate::gate_direction::gate_direction, "gate_direction")?;

qiskit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
sys.modules["qiskit._accelerate.sparse_pauli_op"] = _accelerate.sparse_pauli_op
8181
sys.modules["qiskit._accelerate.star_prerouting"] = _accelerate.star_prerouting
8282
sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap
83+
sys.modules["qiskit._accelerate.elide_permutations"] = _accelerate.elide_permutations
8384
sys.modules["qiskit._accelerate.target"] = _accelerate.target
8485
sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose
8586
sys.modules["qiskit._accelerate.unitary_synthesis"] = _accelerate.unitary_synthesis

qiskit/circuit/library/generalized_gates/permutation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def __init__(
141141
raise CircuitError(
142142
"Permutation pattern must be some ordering of 0..num_qubits-1 in a list."
143143
)
144-
pattern = np.array(pattern)
144+
pattern = np.array(pattern, dtype=np.int32)
145145

146146
super().__init__(name="permutation", num_qubits=num_qubits, params=[pattern])
147147

qiskit/transpiler/passes/optimization/elide_permutations.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515

1616
import logging
1717

18-
from qiskit.circuit.library.standard_gates import SwapGate
19-
from qiskit.circuit.library.generalized_gates import PermutationGate
2018
from qiskit.transpiler.basepasses import TransformationPass
2119
from qiskit.transpiler.layout import Layout
20+
from qiskit._accelerate import elide_permutations as elide_permutations_rs
2221

2322
logger = logging.getLogger(__name__)
2423

@@ -67,38 +66,16 @@ def run(self, dag):
6766
)
6867
return dag
6968

70-
op_count = dag.count_ops(recurse=False)
71-
if op_count.get("swap", 0) == 0 and op_count.get("permutation", 0) == 0:
69+
result = elide_permutations_rs.run(dag)
70+
71+
# If the pass did not do anything, the result is None
72+
if result is None:
7273
return dag
7374

74-
new_dag = dag.copy_empty_like()
75-
qubit_mapping = list(range(len(dag.qubits)))
76-
77-
def _apply_mapping(qargs):
78-
return tuple(dag.qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs)
79-
80-
for node in dag.topological_op_nodes():
81-
if not isinstance(node.op, (SwapGate, PermutationGate)):
82-
new_dag.apply_operation_back(
83-
node.op, _apply_mapping(node.qargs), node.cargs, check=False
84-
)
85-
elif getattr(node.op, "condition", None) is not None:
86-
new_dag.apply_operation_back(
87-
node.op, _apply_mapping(node.qargs), node.cargs, check=False
88-
)
89-
elif isinstance(node.op, SwapGate):
90-
index_0 = dag.find_bit(node.qargs[0]).index
91-
index_1 = dag.find_bit(node.qargs[1]).index
92-
qubit_mapping[index_1], qubit_mapping[index_0] = (
93-
qubit_mapping[index_0],
94-
qubit_mapping[index_1],
95-
)
96-
elif isinstance(node.op, PermutationGate):
97-
starting_indices = [qubit_mapping[dag.find_bit(qarg).index] for qarg in node.qargs]
98-
pattern = node.op.params[0]
99-
updated_indices = [starting_indices[idx] for idx in pattern]
100-
for i, j in zip(starting_indices, updated_indices):
101-
qubit_mapping[i] = j
75+
# Otherwise, the result is a pair consisting of the rewritten DAGCircuit and the
76+
# qubit mapping.
77+
(new_dag, qubit_mapping) = result
78+
10279
input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)}
10380
self.property_set["original_layout"] = Layout(input_qubit_mapping)
10481
if self.property_set["original_qubit_indices"] is None:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features_transpiler:
3+
- |
4+
Port most of the logic of the transpiler pass :class:`~.ElidePermutations`
5+
to Rust.

0 commit comments

Comments
 (0)