diff --git a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs new file mode 100644 index 000000000000..e4498af0f8a5 --- /dev/null +++ b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs @@ -0,0 +1,192 @@ +// 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::prelude::*; +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::imports::{GATE, PARAMETER_VECTOR, 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; + +use crate::equivalence::CircuitFromPython; + +// Custom types +pub type GateIdentifier = (String, u32); +pub type BasisTransformIn = (SmallVec<[Param; 3]>, CircuitFromPython); +pub type BasisTransformOut = (SmallVec<[Param; 3]>, DAGCircuit); + +#[pyfunction(name = "compose_transforms")] +pub(super) fn py_compose_transforms( + py: Python, + basis_transforms: Vec<(GateIdentifier, BasisTransformIn)>, + source_basis: HashSet, + source_dag: &DAGCircuit, +) -> PyResult> { + 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, + source_dag: &'a DAGCircuit, +) -> PyResult> { + let mut gate_param_counts: HashMap = HashMap::default(); + get_gates_num_params(source_dag, &mut gate_param_counts)?; + let mut mapped_instructions: HashMap = HashMap::new(); + + for (gate_name, gate_num_qubits) in source_basis.iter().cloned() { + let num_params = gate_param_counts[&(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::>(), + ))?; + let gate_obj: OperationFromPython = gate.extract()?; + let qubits: Vec = (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::>(), + )) + } else { + None + } + } else { + None + } + }) + .collect::>(); + for (node, params) in nodes_to_replace { + let param_mapping: HashMap = 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::>()?; + 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, + )?; + } + } + } + } + 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, +) -> PyResult<()> { + for node in dag.op_nodes(true) { + if let Some(NodeType::Operation(op)) = dag.dag().node_weight(node) { + 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, +) -> 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(()) +} diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs new file mode 100644 index 000000000000..b97f4e37c4b5 --- /dev/null +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -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::*; + +mod compose_transforms; + +#[pymodule] +pub fn basis_translator(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(compose_transforms::py_compose_transforms))?; + Ok(()) +} diff --git a/crates/accelerate/src/basis/mod.rs b/crates/accelerate/src/basis/mod.rs new file mode 100644 index 000000000000..b072f7848fc2 --- /dev/null +++ b/crates/accelerate/src/basis/mod.rs @@ -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) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(basis_translator::basis_translator))?; + Ok(()) +} diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index e389151b6b04..55e1b0336ae4 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -130,14 +130,14 @@ pub struct Equivalence { #[pyo3(get)] pub params: SmallVec<[Param; 3]>, #[pyo3(get)] - pub circuit: CircuitRep, + pub circuit: CircuitFromPython, } #[pymethods] impl Equivalence { #[new] #[pyo3(signature = (params, circuit))] - fn new(params: SmallVec<[Param; 3]>, circuit: CircuitRep) -> Self { + fn new(params: SmallVec<[Param; 3]>, circuit: CircuitFromPython) -> Self { Self { circuit, params } } @@ -295,15 +295,17 @@ impl<'py> FromPyObject<'py> for GateOper { } } -/// Representation of QuantumCircuit by using an instance of `CircuitData`.] +/// Used to extract an instance of [CircuitData] from a `QuantumCircuit`. +/// It also ensures seamless conversion back to `QuantumCircuit` once sent +/// back to Python. /// /// TODO: Remove this implementation once the `EquivalenceLibrary` is no longer /// called from Python, or once the API is able to seamlessly accept instances -/// of `CircuitData`. +/// of [CircuitData]. #[derive(Debug, Clone)] -pub struct CircuitRep(pub CircuitData); +pub struct CircuitFromPython(pub CircuitData); -impl FromPyObject<'_> for CircuitRep { +impl FromPyObject<'_> for CircuitFromPython { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { if ob.is_instance(QUANTUM_CIRCUIT.get_bound(ob.py()))? { let data: Bound = ob.getattr("_data")?; @@ -318,7 +320,7 @@ impl FromPyObject<'_> for CircuitRep { } } -impl IntoPy for CircuitRep { +impl IntoPy for CircuitFromPython { fn into_py(self, py: Python<'_>) -> PyObject { QUANTUM_CIRCUIT .get_bound(py) @@ -328,7 +330,7 @@ impl IntoPy for CircuitRep { } } -impl ToPyObject for CircuitRep { +impl ToPyObject for CircuitFromPython { fn to_object(&self, py: Python<'_>) -> PyObject { self.clone().into_py(py) } @@ -395,7 +397,7 @@ impl EquivalenceLibrary { &mut self, py: Python, gate: GateOper, - equivalent_circuit: CircuitRep, + equivalent_circuit: CircuitFromPython, ) -> PyResult<()> { self.add_equivalence(py, &gate.operation, &gate.params, equivalent_circuit) } @@ -425,7 +427,12 @@ impl EquivalenceLibrary { /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. #[pyo3(name = "set_entry")] - fn py_set_entry(&mut self, py: Python, gate: GateOper, entry: Vec) -> PyResult<()> { + fn py_set_entry( + &mut self, + py: Python, + gate: GateOper, + entry: Vec, + ) -> PyResult<()> { self.set_entry(py, &gate.operation, &gate.params, entry) } @@ -567,7 +574,7 @@ impl EquivalenceLibrary { py: Python, gate: &PackedOperation, params: &[Param], - equivalent_circuit: CircuitRep, + equivalent_circuit: CircuitFromPython, ) -> PyResult<()> { raise_if_shape_mismatch(gate, &equivalent_circuit)?; raise_if_param_mismatch(py, params, equivalent_circuit.0.unsorted_parameters(py)?)?; @@ -614,7 +621,7 @@ impl EquivalenceLibrary { py: Python, gate: &PackedOperation, params: &[Param], - entry: Vec, + entry: Vec, ) -> PyResult<()> { for equiv in entry.iter() { raise_if_shape_mismatch(gate, equiv)?; @@ -714,7 +721,7 @@ fn raise_if_param_mismatch( Ok(()) } -fn raise_if_shape_mismatch(gate: &PackedOperation, circuit: &CircuitRep) -> PyResult<()> { +fn raise_if_shape_mismatch(gate: &PackedOperation, circuit: &CircuitFromPython) -> PyResult<()> { let op_ref = gate.view(); if op_ref.num_qubits() != circuit.0.num_qubits() as u32 || op_ref.num_clbits() != circuit.0.num_clbits() as u32 @@ -732,7 +739,11 @@ fn raise_if_shape_mismatch(gate: &PackedOperation, circuit: &CircuitRep) -> PyRe Ok(()) } -fn rebind_equiv(py: Python, equiv: Equivalence, query_params: &[Param]) -> PyResult { +fn rebind_equiv( + py: Python, + equiv: Equivalence, + query_params: &[Param], +) -> PyResult { let (equiv_params, mut equiv_circuit) = (equiv.params, equiv.circuit); let param_mapping: PyResult> = equiv_params .iter() diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index aa4776082987..7c347c0a3d75 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -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; diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index dbe8883791f0..693141279f77 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -1001,7 +1001,7 @@ def _format(operand): } /// Add all wires in a quantum register. - fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { + pub fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { if !qreg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { return Err(DAGCircuitError::new_err("not a QuantumRegister instance.")); } @@ -1684,7 +1684,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, @@ -2873,8 +2873,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, diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 5c77f8ef7d17..6e87bcb4f101 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -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 = diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 458d9cc2bde8..2e0929543f1b 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -29,6 +29,7 @@ where #[pymodule] fn _accelerate(m: &Bound) -> 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")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 359095f7a2eb..1566db0be2c2 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -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 diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 0fdae973e1b9..01cbd18c0ee7 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -18,15 +18,11 @@ import logging from functools import singledispatchmethod -from itertools import zip_longest from collections import defaultdict import rustworkx from qiskit.circuit import ( - Gate, - ParameterVector, - QuantumRegister, ControlFlowOp, QuantumCircuit, ParameterExpression, @@ -37,6 +33,7 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit._accelerate.basis.basis_translator import compose_transforms logger = logging.getLogger(__name__) @@ -235,9 +232,9 @@ def run(self, dag): # Compose found path into a set of instruction substitution rules. compose_start_time = time.time() - instr_map = _compose_transforms(basis_transforms, source_basis, dag) + instr_map = compose_transforms(basis_transforms, source_basis, dag) extra_instr_map = { - qarg: _compose_transforms(transforms, qargs_local_source_basis[qarg], dag) + qarg: compose_transforms(transforms, qargs_local_source_basis[qarg], dag) for qarg, transforms in qarg_local_basis_transforms.items() } @@ -486,7 +483,9 @@ def discover_vertex(self, v, score): rule.circuit, score, ) - self._basis_transforms.append((gate.name, gate.num_qubits, rule.params, rule.circuit)) + self._basis_transforms.append( + ((gate.name, gate.num_qubits), (rule.params, rule.circuit)) + ) # we can stop the search if we have found all gates in the original circuit. if not self._source_gates_remain: # if we start from source gates and apply `basis_transforms` in reverse order, we'll end @@ -596,104 +595,10 @@ def _basis_search(equiv_lib, source_basis, target_basis): rtn = vis.basis_transforms logger.debug("Transformation path:") - for gate_name, gate_num_qubits, params, equiv in rtn: + for (gate_name, gate_num_qubits), (params, equiv) in rtn: logger.debug("%s/%s => %s\n%s", gate_name, gate_num_qubits, params, equiv) finally: # Remove dummy node in order to return graph to original state graph.remove_node(dummy) return rtn - - -def _compose_transforms(basis_transforms, source_basis, source_dag): - """Compose a set of basis transforms into a set of replacements. - - Args: - basis_transforms (List[Tuple[gate_name, params, equiv]]): List of - transforms to compose. - source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Names - of gates which need to be translated. - source_dag (DAGCircuit): DAG with example gates from source_basis. - (Used to determine num_params for gate in source_basis.) - - Returns: - Dict[gate_name, Tuple(params, dag)]: Dictionary mapping between each gate - in source_basis and a DAGCircuit instance to replace it. Gates in - source_basis but not affected by basis_transforms will be included - as a key mapping to itself. - """ - example_gates = _get_example_gates(source_dag) - mapped_instrs = {} - - for gate_name, gate_num_qubits in source_basis: - # Need to grab a gate instance to find num_qubits and num_params. - # Can be removed following https://github.com/Qiskit/qiskit-terra/pull/3947 . - example_gate = example_gates[gate_name, gate_num_qubits] - num_params = len(example_gate.params) - - placeholder_params = ParameterVector(gate_name, num_params) - placeholder_gate = Gate(gate_name, gate_num_qubits, list(placeholder_params)) - placeholder_gate.params = list(placeholder_params) - - dag = DAGCircuit() - qr = QuantumRegister(gate_num_qubits) - dag.add_qreg(qr) - dag.apply_operation_back(placeholder_gate, qr, (), check=False) - mapped_instrs[gate_name, gate_num_qubits] = placeholder_params, dag - - for gate_name, gate_num_qubits, equiv_params, equiv in basis_transforms: - logger.debug( - "Composing transform step: %s/%s %s =>\n%s", - gate_name, - gate_num_qubits, - equiv_params, - equiv, - ) - - for mapped_instr_name, (dag_params, dag) in mapped_instrs.items(): - doomed_nodes = [ - node - for node in dag.op_nodes() - if (node.name, node.num_qubits) == (gate_name, gate_num_qubits) - ] - - if doomed_nodes and logger.isEnabledFor(logging.DEBUG): - - logger.debug( - "Updating transform for mapped instr %s %s from \n%s", - mapped_instr_name, - dag_params, - dag_to_circuit(dag, copy_operations=False), - ) - - for node in doomed_nodes: - - replacement = equiv.assign_parameters(dict(zip_longest(equiv_params, node.params))) - - replacement_dag = circuit_to_dag(replacement) - - dag.substitute_node_with_dag(node, replacement_dag) - - if doomed_nodes and logger.isEnabledFor(logging.DEBUG): - - logger.debug( - "Updated transform for mapped instr %s %s to\n%s", - mapped_instr_name, - dag_params, - dag_to_circuit(dag, copy_operations=False), - ) - - return mapped_instrs - - -def _get_example_gates(source_dag): - def recurse(dag, example_gates=None): - example_gates = example_gates or {} - for node in dag.op_nodes(): - example_gates[(node.name, node.num_qubits)] = node - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - example_gates = recurse(circuit_to_dag(block), example_gates) - return example_gates - - return recurse(source_dag)