Skip to content
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

[Oxidize BasisTranslator] Add rust-native compose_transforms() #13137

Merged
merged 21 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f9e97ea
Initial: Add `compose_transforms` and `get_example_gates` to Rust.
raynelfss Aug 28, 2024
3a3e734
Add: `compose_transform` logic in rust
raynelfss Sep 5, 2024
0589a02
Fix: Correct the behavior of `compose_transforms`.
raynelfss Sep 9, 2024
1a49bfd
Merge main
raynelfss Sep 11, 2024
fbb57b8
Fix: Leverage rust-based `circuit_to_dag` converters.
raynelfss Sep 11, 2024
e8c957d
Merge branch 'main' into rust-compose-trans
raynelfss Sep 11, 2024
a7d2f17
Format: Fix indentation of import lines in `compose_transforms.rs`
raynelfss Sep 11, 2024
8e96eb7
Formatting: Separate complicated type aliases
raynelfss Sep 12, 2024
18591ce
Merge branch 'main' into rust-compose-trans
raynelfss Sep 12, 2024
676766d
Fix: Adapt to new `DAGCircuit` limitations
raynelfss Sep 12, 2024
1b16df6
Merge branch 'main' into rust-compose-trans
raynelfss Sep 12, 2024
a2cb862
Format: Remove unused imports in BasisTranslator
raynelfss Sep 12, 2024
c9082c7
Merge branch 'main' into rust-compose-trans
raynelfss Sep 12, 2024
3d1a6d4
Merge branch 'Qiskit:main' into rust-compose-trans
raynelfss Sep 18, 2024
956c425
Fix: Adapt to #13143
raynelfss Sep 18, 2024
9c6913b
Merge branch 'main' into rust-compose-trans
raynelfss Sep 20, 2024
f818ad2
Fix: Code review comments
raynelfss Sep 20, 2024
1e94449
Fix: More commments from code review
raynelfss Sep 24, 2024
912390e
Merge branch 'main' into rust-compose-trans
raynelfss Sep 24, 2024
98cebda
Merge branch 'main' into rust-compose-trans
raynelfss Sep 26, 2024
dbdc6a9
Refactor: Rename `example_gates` to `gate_param_counts`
raynelfss Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions crates/accelerate/src/basis/basis_translator/compose_transforms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// 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 hashbrown::{HashMap, HashSet};
use pyo3::{exceptions::PyTypeError, prelude::*};
use qiskit_circuit::circuit_instruction::OperationFromPython;
use qiskit_circuit::imports::{GATE, PARAMETER_VECTOR, QUANTUM_CIRCUIT, QUANTUM_REGISTER};
use qiskit_circuit::parameter_table::ParameterUuid;
use qiskit_circuit::Qubit;
use qiskit_circuit::{
circuit_data::CircuitData,
dag_circuit::{DAGCircuit, NodeType},
operations::{Operation, Param},
};
use smallvec::SmallVec;

// Custom types
pub type GateIdentifier = (String, u32);
pub type BasisTransformIn = (SmallVec<[Param; 3]>, CircuitFromPython);
pub type BasisTransformOut = (SmallVec<[Param; 3]>, DAGCircuit);
// TODO: Remove these and use the version from `EquivalenceLibrary`

/// Representation of QuantumCircuit which the original circuit object + an
/// instance of `CircuitData`.
raynelfss marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug, Clone)]
pub struct CircuitFromPython(pub CircuitData);

impl FromPyObject<'_> for CircuitFromPython {
fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Self> {
if ob.is_instance(QUANTUM_CIRCUIT.get_bound(ob.py()))? {
let data: CircuitData = ob.getattr("_data")?.extract()?;
Ok(Self(data))
} else {
Err(PyTypeError::new_err(
"Provided object was not an instance of QuantumCircuit",
))
}
}
}

#[pyfunction(name = "compose_transforms")]
pub(super) fn py_compose_transforms(
py: Python,
basis_transforms: Vec<(GateIdentifier, BasisTransformIn)>,
source_basis: HashSet<GateIdentifier>,
source_dag: &DAGCircuit,
) -> PyResult<HashMap<GateIdentifier, BasisTransformOut>> {
compose_transforms(py, &basis_transforms, &source_basis, source_dag)
}

pub(super) fn compose_transforms<'a>(
py: Python,
basis_transforms: &'a [(GateIdentifier, BasisTransformIn)],
source_basis: &'a HashSet<GateIdentifier>,
source_dag: &'a DAGCircuit,
) -> PyResult<HashMap<GateIdentifier, BasisTransformOut>> {
let mut example_gates: HashMap<GateIdentifier, usize> = HashMap::default();
raynelfss marked this conversation as resolved.
Show resolved Hide resolved
get_gates_num_params(source_dag, &mut example_gates)?;
let mut mapped_instructions: HashMap<GateIdentifier, BasisTransformOut> = HashMap::new();

for (gate_name, gate_num_qubits) in source_basis.iter().cloned() {
let num_params = example_gates[&(gate_name.clone(), gate_num_qubits)];

let placeholder_params: SmallVec<[Param; 3]> = PARAMETER_VECTOR
.get_bound(py)
.call1((&gate_name, num_params))?
.extract()?;

let mut dag = DAGCircuit::new(py)?;
// Create the mock gate and add to the circuit, use Python for this.
let qubits = QUANTUM_REGISTER.get_bound(py).call1((gate_num_qubits,))?;
dag.add_qreg(py, &qubits)?;

let gate = GATE.get_bound(py).call1((
&gate_name,
gate_num_qubits,
placeholder_params
.iter()
.map(|x| x.clone_ref(py))
.collect::<SmallVec<[Param; 3]>>(),
))?;
let gate_obj: OperationFromPython = gate.extract()?;
let qubits: Vec<Qubit> = (0..dag.num_qubits() as u32).map(Qubit).collect();
dag.apply_operation_back(
py,
gate_obj.operation,
&qubits,
&[],
if gate_obj.params.is_empty() {
None
} else {
Some(gate_obj.params)
},
gate_obj.extra_attrs,
#[cfg(feature = "cache_pygates")]
Some(gate.into()),
)?;
mapped_instructions.insert((gate_name, gate_num_qubits), (placeholder_params, dag));

for ((gate_name, gate_num_qubits), (equiv_params, equiv)) in basis_transforms {
for (_, dag) in &mut mapped_instructions.values_mut() {
let nodes_to_replace = dag
.op_nodes(true)
.filter_map(|node| {
if let Some(NodeType::Operation(op)) = dag.dag().node_weight(node) {
if (gate_name.as_str(), *gate_num_qubits)
== (op.op.name(), op.op.num_qubits())
{
Some((
node,
op.params_view()
.iter()
.map(|x| x.clone_ref(py))
.collect::<SmallVec<[Param; 3]>>(),
))
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
for (node, params) in nodes_to_replace {
let param_mapping: HashMap<ParameterUuid, Param> = equiv_params
.iter()
.map(|x| ParameterUuid::from_parameter(x.to_object(py).bind(py)))
.zip(params)
.map(|(uuid, param)| -> PyResult<(ParameterUuid, Param)> {
Ok((uuid?, param.clone_ref(py)))
})
.collect::<PyResult<_>>()?;
let mut replacement = equiv.clone();
replacement
.0
.assign_parameters_from_mapping(py, param_mapping)?;
let replace_dag: DAGCircuit =
DAGCircuit::from_circuit_data(py, replacement.0, true)?;
let op_node = dag.get_node(py, node)?;
dag.py_substitute_node_with_dag(
py,
op_node.bind(py),
&replace_dag,
None,
true,
)?;
Comment on lines +131 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this is slower than it ought to be, but it'd be non-trivial to expose a native-Rust substitute_node_with_dag. Maybe we can revisit doing so in a future PR.

}
}
}
}
Ok(mapped_instructions)
}

/// `DAGCircuit` variant.
///
/// Gets the identifier of a gate instance (name, number of qubits) mapped to the
/// number of parameters it contains currently.
fn get_gates_num_params(
dag: &DAGCircuit,
example_gates: &mut HashMap<GateIdentifier, usize>,
) -> PyResult<()> {
for node in dag.op_nodes(true) {
if let Some(NodeType::Operation(op)) = dag.dag().node_weight(node) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we now have NodeType::unwrap_operation so you can use that here instead of the if let.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example_gates.insert(
(op.op.name().to_string(), op.op.num_qubits()),
op.params_view().len(),
);
if op.op.control_flow() {
let blocks = op.op.blocks();
for block in blocks {
get_gates_num_params_circuit(&block, example_gates)?;
}
}
}
}
Ok(())
}

/// `CircuitData` variant.
///
/// Gets the identifier of a gate instance (name, number of qubits) mapped to the
/// number of parameters it contains currently.
fn get_gates_num_params_circuit(
circuit: &CircuitData,
example_gates: &mut HashMap<GateIdentifier, usize>,
) -> PyResult<()> {
for inst in circuit.iter() {
example_gates.insert(
(inst.op.name().to_string(), inst.op.num_qubits()),
inst.params_view().len(),
);
if inst.op.control_flow() {
let blocks = inst.op.blocks();
for block in blocks {
get_gates_num_params_circuit(&block, example_gates)?;
}
}
}
Ok(())
}
21 changes: 21 additions & 0 deletions crates/accelerate/src/basis/basis_translator/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// This code is part of Qiskit.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a clear benefit to having a separate folder here for basis_translator?

I don't know that I mind it, but it's different from the folder structure we had in Python, and none of the other accelerate modules really go more than one module deep (except for synthesis, which seems to copy the organization of the Python module).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just to plan ahead, since the BasisTranslator has a couple of moving parts and since I am separating things at the moment to make them more readable. If you take a look at #12811, that feature also exists in that same folder. but in a different file If you think it shouldn't be this way it can be changed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It suppose it doesn't really matter much, so if you think it helps to organize I'm good with it 🙂.

//
// (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 pyo3::prelude::*;

mod compose_transforms;

#[pymodule]
pub fn basis_translator(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(compose_transforms::py_compose_transforms))?;
Ok(())
}
21 changes: 21 additions & 0 deletions crates/accelerate/src/basis/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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 pyo3::{prelude::*, wrap_pymodule};

pub mod basis_translator;

#[pymodule]
pub fn basis(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pymodule!(basis_translator::basis_translator))?;
Ok(())
}
1 change: 1 addition & 0 deletions crates/accelerate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::env;
use pyo3::import_exception;

pub mod barrier_before_final_measurement;
pub mod basis;
pub mod check_map;
pub mod circuit_library;
pub mod commutation_analysis;
Expand Down
8 changes: 4 additions & 4 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ def _format(operand):
}

/// Add all wires in a quantum register.
fn add_qreg(&mut self, py: Python, qreg: &Bound<PyAny>) -> PyResult<()> {
pub fn add_qreg(&mut self, py: Python, qreg: &Bound<PyAny>) -> PyResult<()> {
if !qreg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? {
return Err(DAGCircuitError::new_err("not a QuantumRegister instance."));
}
Expand Down Expand Up @@ -1671,7 +1671,7 @@ def _format(operand):
/// Raises:
/// DAGCircuitError: if a leaf node is connected to multiple outputs
#[pyo3(name = "apply_operation_back", signature = (op, qargs=None, cargs=None, *, check=true))]
fn py_apply_operation_back(
pub fn py_apply_operation_back(
&mut self,
py: Python,
op: Bound<PyAny>,
Expand Down Expand Up @@ -2860,8 +2860,8 @@ def _format(operand):
///
/// Raises:
/// DAGCircuitError: if met with unexpected predecessor/successors
#[pyo3(signature = (node, input_dag, wires=None, propagate_condition=true))]
fn substitute_node_with_dag(
#[pyo3(name = "substitute_node_with_dag", signature = (node, input_dag, wires=None, propagate_condition=true))]
pub fn py_substitute_node_with_dag(
&mut self,
py: Python,
node: &Bound<PyAny>,
Expand Down
2 changes: 2 additions & 0 deletions crates/circuit/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub static CLASSICAL_REGISTER: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.classicalregister", "ClassicalRegister");
pub static PARAMETER_EXPRESSION: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression");
pub static PARAMETER_VECTOR: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.parametervector", "ParameterVector");
pub static QUANTUM_CIRCUIT: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit");
pub static SINGLETON_GATE: ImportOnceCell =
Expand Down
1 change: 1 addition & 0 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ where
#[pymodule]
fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
add_submodule(m, ::qiskit_accelerate::barrier_before_final_measurement::barrier_before_final_measurements_mod, "barrier_before_final_measurement")?;
add_submodule(m, ::qiskit_accelerate::basis::basis, "basis")?;
add_submodule(m, ::qiskit_accelerate::check_map::check_map_mod, "check_map")?;
add_submodule(m, ::qiskit_accelerate::circuit_library::circuit_library, "circuit_library")?;
add_submodule(m, ::qiskit_accelerate::commutation_analysis::commutation_analysis, "commutation_analysis")?;
Expand Down
2 changes: 2 additions & 0 deletions qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
# and not have to rely on attribute access. No action needed for top-level extension packages.
sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit
sys.modules["qiskit._accelerate.circuit_library"] = _accelerate.circuit_library
sys.modules["qiskit._accelerate.basis"] = _accelerate.basis
sys.modules["qiskit._accelerate.basis.basis_translator"] = _accelerate.basis.basis_translator
sys.modules["qiskit._accelerate.converters"] = _accelerate.converters
sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix
sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout
Expand Down
Loading
Loading