From 254ba83dc637a22a91cee0a0cd1c453f89ca55da Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:36:03 -0400 Subject: [PATCH 01/11] Fix documentation for utils.optionals data values (#13098) --- qiskit/utils/optionals.py | 323 ++++++++++++++++++++------------------ 1 file changed, 173 insertions(+), 150 deletions(-) diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index f2e6c860faaa..c5d0d69d11a1 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -25,172 +25,195 @@ Qiskit Components ----------------- -.. list-table:: - :widths: 25 75 +.. py:data:: HAS_AER - * - .. py:data:: HAS_AER - - `Qiskit Aer ` provides high-performance simulators for - the quantum circuits constructed within Qiskit. + `Qiskit Aer ` provides high-performance simulators for + the quantum circuits constructed within Qiskit. - * - .. py:data:: HAS_IBMQ - - The :mod:`Qiskit IBMQ Provider ` is used for accessing IBM Quantum - hardware in the IBM cloud. +.. py:data:: HAS_IBMQ - * - .. py:data:: HAS_IGNIS - - :mod:`Qiskit Ignis ` provides tools for quantum hardware verification, noise - characterization, and error correction. + The :mod:`Qiskit IBMQ Provider ` is used for accessing IBM Quantum + hardware in the IBM cloud. - * - .. py:data:: HAS_TOQM - - `Qiskit TOQM `__ provides transpiler passes - for the `Time-optimal Qubit mapping algorithm `__. +.. py:data:: HAS_IGNIS + + :mod:`Qiskit Ignis ` provides tools for quantum hardware verification, noise + characterization, and error correction. + +.. py:data:: HAS_TOQM + + `Qiskit TOQM `__ provides transpiler passes + for the `Time-optimal Qubit mapping algorithm `__. External Python Libraries ------------------------- -.. list-table:: - :widths: 25 75 - - * - .. py:data:: HAS_CONSTRAINT - - `python-constraint `__ is a - constraint satisfaction problem solver, used in the :class:`~.CSPLayout` transpiler pass. - - * - .. py:data:: HAS_CPLEX - - The `IBM CPLEX Optimizer `__ is a - high-performance mathematical programming solver for linear, mixed-integer and quadratic - programming. This is no longer by Qiskit, but it weas historically and the optional - remains for backwards compatibility. - - * - .. py:data:: HAS_CVXPY - - `CVXPY `__ is a Python package for solving convex optimization - problems. It is required for calculating diamond norms with - :func:`.quantum_info.diamond_norm`. - - * - .. py:data:: HAS_DOCPLEX - - `IBM Decision Optimization CPLEX Modelling - `__ is a library for prescriptive - analysis. Like CPLEX, this is no longer by Qiskit, but it weas historically and the - optional remains for backwards compatibility. - - * - .. py:data:: HAS_FIXTURES - - The test suite has additional features that are available if the optional `fixtures - `__ module is installed. This generally also needs - :data:`HAS_TESTTOOLS` as well. This is generally only needed for Qiskit developers. - - * - .. py:data:: HAS_IPYTHON - - If `the IPython kernel `__ is available, certain additional - visualizations and line magics are made available. - - * - .. py:data:: HAS_IPYWIDGETS - - Monitoring widgets for jobs running on external backends can be provided if `ipywidgets - `__ is available. - - * - .. py:data:: HAS_JAX - - Some methods of gradient calculation within :mod:`.opflow.gradients` require `JAX - `__ for autodifferentiation. - - * - .. py:data:: HAS_JUPYTER - - Some of the tests require a complete `Jupyter `__ installation to test - interactivity features. - - * - .. py:data:: HAS_MATPLOTLIB - - Qiskit provides several visualization tools in the :mod:`.visualization` module. - Almost all of these are built using `Matplotlib `__, which must - be installed in order to use them. - - * - .. py:data:: HAS_NETWORKX - - No longer used by Qiskit. Internally, Qiskit now uses the high-performance `rustworkx - `__ library as a core dependency, and during the - change-over period, it was sometimes convenient to convert things into the Python-only - `NetworkX `__ format. Some tests of application modules, such as - `Qiskit Nature `__ still use NetworkX. - - * - .. py:data:: HAS_NLOPT - - `NLopt `__ is a nonlinear optimization library, - used by the global optimizers in the :mod:`.algorithms.optimizers` module. - - * - .. py:data:: HAS_PIL - - PIL is a Python image-manipulation library. Qiskit actually uses the `pillow - `__ fork of PIL if it is available when generating - certain visualizations, for example of both :class:`.QuantumCircuit` and - :class:`.DAGCircuit` in certain modes. - - * - .. py:data:: HAS_PYDOT - - For some graph visualizations, Qiskit uses `pydot `__ as an - interface to GraphViz (see :data:`HAS_GRAPHVIZ`). - - * - .. py:data:: HAS_PYGMENTS - - Pygments is a code highlighter and formatter used by many environments that involve rich - display of code blocks, including Sphinx and Jupyter. Qiskit uses this when producing rich - output for these environments. - - * - .. py:data:: HAS_PYLATEX - - Various LaTeX-based visualizations, especially the circuit drawers, need access to the - `pylatexenc `__ project to work correctly. - - * - .. py:data:: HAS_QASM3_IMPORT - - The functions :func:`.qasm3.load` and :func:`.qasm3.loads` for importing OpenQASM 3 programs - into :class:`.QuantumCircuit` instances use `an external importer package - `__. - - * - .. py:data:: HAS_SEABORN - - Qiskit provides several visualization tools in the :mod:`.visualization` module. Some - of these are built using `Seaborn `__, which must be installed - in order to use them. - - * - .. py:data:: HAS_SKLEARN - - Some of the gradient functions in :mod:`.opflow.gradients` use regularisation methods from - `Scikit Learn `__. - - * - .. py:data:: HAS_SKQUANT - - Some of the optimisers in :mod:`.algorithms.optimizers` are based on those found in `Scikit - Quant `__, which must be installed to use - them. - - * - .. py:data:: HAS_SQSNOBFIT - - `SQSnobFit `__ is a library for the "stable noisy - optimization by branch and fit" algorithm. It is used by the :class:`.SNOBFIT` optimizer. - - * - .. py:data:: HAS_SYMENGINE - - `Symengine `__ is a fast C++ backend for the - symbolic-manipulation library `Sympy `__. Qiskit uses - special methods from Symengine to accelerate its handling of - :class:`~.circuit.Parameter`\\ s if available. - - * - .. py:data:: HAS_TESTTOOLS - - Qiskit's test suite has more advanced functionality available if the optional - `testtools `__ library is installed. This is generally - only needed for Qiskit developers. - - * - .. py:data:: HAS_TWEEDLEDUM - - `Tweedledum `__ is an extension library for - synthesis and optimization of circuits that may involve classical oracles. Qiskit's - :class:`.PhaseOracle` uses this, which is used in turn by amplification algorithms via - the :class:`.AmplificationProblem`. - - * - .. py:data:: HAS_Z3 - - `Z3 `__ is a theorem prover, used in the - :class:`.CrosstalkAdaptiveSchedule` and :class:`.HoareOptimizer` transpiler passes. +.. py:data:: HAS_CONSTRAINT + + `python-constraint `__ is a + constraint satisfaction problem solver, used in the :class:`~.CSPLayout` transpiler pass. + +.. py:data:: HAS_CPLEX + + The `IBM CPLEX Optimizer `__ is a + high-performance mathematical programming solver for linear, mixed-integer and quadratic + programming. This is no longer by Qiskit, but it weas historically and the optional + remains for backwards compatibility. + +.. py:data:: HAS_CVXPY + + `CVXPY `__ is a Python package for solving convex optimization + problems. It is required for calculating diamond norms with + :func:`.quantum_info.diamond_norm`. + +.. py:data:: HAS_DOCPLEX + + `IBM Decision Optimization CPLEX Modelling + `__ is a library for prescriptive + analysis. Like CPLEX, this is no longer by Qiskit, but it weas historically and the + optional remains for backwards compatibility. + +.. py:data:: HAS_FIXTURES + + The test suite has additional features that are available if the optional `fixtures + `__ module is installed. This generally also needs + :data:`HAS_TESTTOOLS` as well. This is generally only needed for Qiskit developers. + +.. py:data:: HAS_IPYTHON + + If `the IPython kernel `__ is available, certain additional + visualizations and line magics are made available. + +.. py:data:: HAS_IPYWIDGETS + + Monitoring widgets for jobs running on external backends can be provided if `ipywidgets + `__ is available. + +.. py:data:: HAS_JAX + + Some methods of gradient calculation within :mod:`.opflow.gradients` require `JAX + `__ for autodifferentiation. + +.. py:data:: HAS_JUPYTER + + Some of the tests require a complete `Jupyter `__ installation to test + interactivity features. + +.. py:data:: HAS_MATPLOTLIB + + Qiskit provides several visualization tools in the :mod:`.visualization` module. + Almost all of these are built using `Matplotlib `__, which must + be installed in order to use them. + +.. py:data:: HAS_NETWORKX + + No longer used by Qiskit. Internally, Qiskit now uses the high-performance `rustworkx + `__ library as a core dependency, and during the + change-over period, it was sometimes convenient to convert things into the Python-only + `NetworkX `__ format. Some tests of application modules, such as + `Qiskit Nature `__ still use NetworkX. + +.. py:data:: HAS_NLOPT + + `NLopt `__ is a nonlinear optimization library, + used by the global optimizers in the :mod:`.algorithms.optimizers` module. + +.. py:data:: HAS_PIL + + PIL is a Python image-manipulation library. Qiskit actually uses the `pillow + `__ fork of PIL if it is available when generating + certain visualizations, for example of both :class:`.QuantumCircuit` and + :class:`.DAGCircuit` in certain modes. + +.. py:data:: HAS_PYDOT + + For some graph visualizations, Qiskit uses `pydot `__ as an + interface to GraphViz (see :data:`HAS_GRAPHVIZ`). + +.. py:data:: HAS_PYGMENTS + + Pygments is a code highlighter and formatter used by many environments that involve rich + display of code blocks, including Sphinx and Jupyter. Qiskit uses this when producing rich + output for these environments. + +.. py:data:: HAS_PYLATEX + + Various LaTeX-based visualizations, especially the circuit drawers, need access to the + `pylatexenc `__ project to work correctly. + +.. py:data:: HAS_QASM3_IMPORT + + The functions :func:`.qasm3.load` and :func:`.qasm3.loads` for importing OpenQASM 3 programs + into :class:`.QuantumCircuit` instances use `an external importer package + `__. + +.. py:data:: HAS_SEABORN + + Qiskit provides several visualization tools in the :mod:`.visualization` module. Some + of these are built using `Seaborn `__, which must be installed + in order to use them. + +.. py:data:: HAS_SKLEARN + + Some of the gradient functions in :mod:`.opflow.gradients` use regularisation methods from + `Scikit Learn `__. + +.. py:data:: HAS_SKQUANT + + Some of the optimisers in :mod:`.algorithms.optimizers` are based on those found in `Scikit + Quant `__, which must be installed to use + them. + +.. py:data:: HAS_SQSNOBFIT + + `SQSnobFit `__ is a library for the "stable noisy + optimization by branch and fit" algorithm. It is used by the :class:`.SNOBFIT` optimizer. + +.. py:data:: HAS_SYMENGINE + + `Symengine `__ is a fast C++ backend for the + symbolic-manipulation library `Sympy `__. Qiskit uses + special methods from Symengine to accelerate its handling of + :class:`~.circuit.Parameter`\\ s if available. + +.. py:data:: HAS_TESTTOOLS + + Qiskit's test suite has more advanced functionality available if the optional + `testtools `__ library is installed. This is generally + only needed for Qiskit developers. + +.. py:data:: HAS_TWEEDLEDUM + + `Tweedledum `__ is an extension library for + synthesis and optimization of circuits that may involve classical oracles. Qiskit's + :class:`.PhaseOracle` uses this, which is used in turn by amplification algorithms via + the :class:`.AmplificationProblem`. + +.. py:data:: HAS_Z3 + + `Z3 `__ is a theorem prover, used in the + :class:`.CrosstalkAdaptiveSchedule` and :class:`.HoareOptimizer` transpiler passes. External Command-Line Tools --------------------------- -.. list-table:: - :widths: 25 75 +.. py:data:: HAS_GRAPHVIZ + + For some graph visualizations, Qiskit uses the `GraphViz `__ + visualization tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). + +.. py:data:: HAS_PDFLATEX - * - .. py:data:: HAS_GRAPHVIZ - - For some graph visualizations, Qiskit uses the `GraphViz `__ - visualization tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). + Visualization tools that use LaTeX in their output, such as the circuit drawers, require + ``pdflatex`` to be available. You will generally need to ensure that you have a working + LaTeX installation available, and the ``qcircuit.tex`` package. - * - .. py:data:: HAS_PDFLATEX - - Visualization tools that use LaTeX in their output, such as the circuit drawers, require - ``pdflatex`` to be available. You will generally need to ensure that you have a working - LaTeX installation available, and the ``qcircuit.tex`` package. +.. py:data:: HAS_PDFTOCAIRO - * - .. py:data:: HAS_PDFTOCAIRO - - Visualization tools that convert LaTeX-generated files into rasterized images use the - ``pdftocairo`` tool. This is part of the `Poppler suite of PDF tools - `__. + Visualization tools that convert LaTeX-generated files into rasterized images use the + ``pdftocairo`` tool. This is part of the `Poppler suite of PDF tools + `__. Lazy Checker Classes From 8982dbde157eafa8e104495472115f4257a502fa Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 5 Sep 2024 19:00:08 -0400 Subject: [PATCH 02/11] Fully port FilterOpNodes to Rust (#13052) * Fully port FilterOpNodes to Rust This commit ports the FilterOpNodes pass to rust. This pass is exceedingly simple and just runs a filter function over all the op nodes and removes nodes that match the filter. However, the API for the class exposes that filter function interface as a user provided Python callable. So for the current pass we need to retain that python callback. This limits the absolute performance of this pass because we're bottlenecked by calling python. Looking to the future, this commit adds a rust native method to DAGCircuit to perform this filtering with a rust predicate FnMut. It isn't leveraged by the python implementation because of layer mismatch for the efficient rust interface and Python working with `DAGOpNode` objects. A function using that interface is added to filter labeled nodes. In the preset pass manager we only use FilterOpNodes to remove nodes with a specific label (which is used to identify temporary barriers created by qiskit). In a follow up we should consider leveraging this new function to build a new pass specifically for this use case. Fixes #12263 Part of #12208 * Make filter_op_nodes() infallible The filter_op_nodes() method originally returned a Result<()> to handle a predicate that was fallible. This was because the original intent for the method was to use it with Python callbacks in the predicate. But because of differences between the rust API and the Python API this wasn't feasible as was originally planned. So this Result<()> return wasn't used anymore. This commit reworks it to make the filter_op_nodes() infallible and the predicate a user provides also only returns `bool` and not `Result`. * Rename filter_labelled_op to filter_labeled_op --- crates/accelerate/src/filter_op_nodes.rs | 63 +++++++++++++++++++ crates/accelerate/src/lib.rs | 1 + .../remove_diagonal_gates_before_measure.rs | 4 +- crates/circuit/src/dag_circuit.rs | 19 ++++++ crates/circuit/src/packed_instruction.rs | 7 +++ crates/pyext/src/lib.rs | 6 +- qiskit/__init__.py | 1 + .../passes/utils/filter_op_nodes.py | 6 +- 8 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 crates/accelerate/src/filter_op_nodes.rs diff --git a/crates/accelerate/src/filter_op_nodes.rs b/crates/accelerate/src/filter_op_nodes.rs new file mode 100644 index 000000000000..7c41391f3788 --- /dev/null +++ b/crates/accelerate/src/filter_op_nodes.rs @@ -0,0 +1,63 @@ +// 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::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::packed_instruction::PackedInstruction; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +#[pyfunction] +#[pyo3(name = "filter_op_nodes")] +pub fn py_filter_op_nodes( + py: Python, + dag: &mut DAGCircuit, + predicate: &Bound, +) -> PyResult<()> { + let callable = |node: NodeIndex| -> PyResult { + let dag_op_node = dag.get_node(py, node)?; + predicate.call1((dag_op_node,))?.extract() + }; + let mut remove_nodes: Vec = Vec::new(); + for node in dag.op_nodes(true) { + if !callable(node)? { + remove_nodes.push(node); + } + } + for node in remove_nodes { + dag.remove_op_node(node); + } + Ok(()) +} + +/// Remove any nodes that have the provided label set +/// +/// Args: +/// dag (DAGCircuit): The dag circuit to filter the ops from +/// label (str): The label to filter nodes on +#[pyfunction] +pub fn filter_labeled_op(dag: &mut DAGCircuit, label: String) { + let predicate = |node: &PackedInstruction| -> bool { + match node.label() { + Some(inst_label) => inst_label != label, + None => false, + } + }; + dag.filter_op_nodes(predicate); +} + +pub fn filter_op_nodes_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(py_filter_op_nodes))?; + m.add_wrapped(wrap_pyfunction!(filter_labeled_op))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index e8760ee2c616..78eea97faad0 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -22,6 +22,7 @@ pub mod dense_layout; pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; +pub mod filter_op_nodes; pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; diff --git a/crates/accelerate/src/remove_diagonal_gates_before_measure.rs b/crates/accelerate/src/remove_diagonal_gates_before_measure.rs index 10916a77fca8..cf2c738f131a 100644 --- a/crates/accelerate/src/remove_diagonal_gates_before_measure.rs +++ b/crates/accelerate/src/remove_diagonal_gates_before_measure.rs @@ -49,7 +49,9 @@ fn run_remove_diagonal_before_measure(dag: &mut DAGCircuit) -> PyResult<()> { let mut nodes_to_remove = Vec::new(); for index in dag.op_nodes(true) { let node = &dag.dag[index]; - let NodeType::Operation(inst) = node else {panic!()}; + let NodeType::Operation(inst) = node else { + panic!() + }; if inst.op.name() == "measure" { let predecessor = (dag.quantum_predecessors(index)) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 381ef25b7a7e..32b3a77ed24c 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -5794,6 +5794,25 @@ impl DAGCircuit { } } + // Filter any nodes that don't match a given predicate function + pub fn filter_op_nodes(&mut self, mut predicate: F) + where + F: FnMut(&PackedInstruction) -> bool, + { + let mut remove_nodes: Vec = Vec::new(); + for node in self.op_nodes(true) { + let NodeType::Operation(op) = &self.dag[node] else { + unreachable!() + }; + if !predicate(op) { + remove_nodes.push(node); + } + } + for node in remove_nodes { + self.remove_op_node(node); + } + } + pub fn op_nodes_by_py_type<'a>( &'a self, op: &'a Bound, diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 77ca0c6c02dd..df8f9801314a 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -553,6 +553,13 @@ impl PackedInstruction { .and_then(|extra| extra.condition.as_ref()) } + #[inline] + pub fn label(&self) -> Option<&str> { + self.extra_attrs + .as_ref() + .and_then(|extra| extra.label.as_deref()) + } + /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this /// instruction. This may construct the reference if the `PackedInstruction` is a standard /// gate with no already stored operation. diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 1478fb367a13..49e44bffa2ec 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -16,8 +16,9 @@ use qiskit_accelerate::{ circuit_library::circuit_library, commutation_analysis::commutation_analysis, commutation_checker::commutation_checker, 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, - optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, + euler_one_qubit_decomposer::euler_one_qubit_decomposer, filter_op_nodes::filter_op_nodes_mod, + isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, + pauli_exp_val::pauli_expval, remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, 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, @@ -46,6 +47,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, filter_op_nodes_mod, "filter_op_nodes")?; 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 d9979c9d4d92..3cc10bf96a31 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -92,6 +92,7 @@ sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/transpiler/passes/utils/filter_op_nodes.py b/qiskit/transpiler/passes/utils/filter_op_nodes.py index 344d2280e3f4..75b824332aee 100644 --- a/qiskit/transpiler/passes/utils/filter_op_nodes.py +++ b/qiskit/transpiler/passes/utils/filter_op_nodes.py @@ -18,6 +18,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils import control_flow +from qiskit._accelerate.filter_op_nodes import filter_op_nodes + class FilterOpNodes(TransformationPass): """Remove all operations that match a filter function @@ -59,7 +61,5 @@ def __init__(self, predicate: Callable[[DAGOpNode], bool]): @control_flow.trivial_recurse def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the RemoveBarriers pass on `dag`.""" - for node in dag.op_nodes(): - if not self.predicate(node): - dag.remove_op_node(node) + filter_op_nodes(dag, self.predicate) return dag From b80b454c8763f650b134a514faf959aee51d9dd3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 5 Sep 2024 19:29:26 -0400 Subject: [PATCH 03/11] Fix clippy failures with Rust 1.81.0 (#13100) The recently released Rust 1.81.0 introduced some new on by default clippy rules and these rules are flagging issues in the rust code in the library. While we use Rust 1.70 for clippy in CI and these won't cause failures until we raise our MSRV to >= 1.81.0 these clippy warnings/failures are still good to fix as the either make the code more consise and/or efficient. This commit fixes these issues identified by clippy. --- crates/accelerate/src/results/marginalization.rs | 9 ++------- crates/circuit/src/operations.rs | 5 +---- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/accelerate/src/results/marginalization.rs b/crates/accelerate/src/results/marginalization.rs index 5260287e771d..b9e7ec4ba65c 100644 --- a/crates/accelerate/src/results/marginalization.rs +++ b/crates/accelerate/src/results/marginalization.rs @@ -26,16 +26,11 @@ fn marginalize( indices: Option>, ) -> HashMap { let mut out_counts: HashMap = HashMap::with_capacity(counts.len()); - let clbit_size = counts - .keys() - .next() - .unwrap() - .replace(|c| c == '_' || c == ' ', "") - .len(); + let clbit_size = counts.keys().next().unwrap().replace(['_', ' '], "").len(); let all_indices: Vec = (0..clbit_size).collect(); counts .iter() - .map(|(k, v)| (k.replace(|c| c == '_' || c == ' ', ""), *v)) + .map(|(k, v)| (k.replace(['_', ' '], ""), *v)) .for_each(|(k, v)| match &indices { Some(indices) => { if all_indices == *indices { diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 8cc2256af518..a3fd2fc83c3d 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -2225,10 +2225,7 @@ impl Operation for PyGate { fn standard_gate(&self) -> Option { Python::with_gil(|py| -> Option { match self.gate.getattr(py, intern!(py, "_standard_gate")) { - Ok(stdgate) => match stdgate.extract(py) { - Ok(out_gate) => out_gate, - Err(_) => None, - }, + Ok(stdgate) => stdgate.extract(py).unwrap_or_default(), Err(_) => None, } }) From 1a4193a1c52f73f081407b8f234da1b154198da6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:08:35 +0000 Subject: [PATCH 04/11] Bump bytemuck from 1.17.1 to 1.18.0 (#13105) Bumps [bytemuck](https://github.com/Lokathor/bytemuck) from 1.17.1 to 1.18.0. - [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md) - [Commits](https://github.com/Lokathor/bytemuck/compare/v1.17.1...v1.18.0) --- updated-dependencies: - dependency-name: bytemuck dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7921b7aeba83..9745b67b7405 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" dependencies = [ "bytemuck_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 4f7f8ad86b96..2f5fd8ce077b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ license = "Apache-2.0" # # Each crate can add on specific features freely as it inherits. [workspace.dependencies] -bytemuck = "1.17" +bytemuck = "1.18" indexmap.version = "2.5.0" hashbrown.version = "0.14.5" num-bigint = "0.4" From 7c409123a369c7df0b86f99ae00fefd41bc142fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:34:52 +0000 Subject: [PATCH 05/11] Bump faer from 0.19.2 to 0.19.3 (#13104) Bumps [faer](https://github.com/sarah-ek/faer-rs) from 0.19.2 to 0.19.3. - [Changelog](https://github.com/sarah-ek/faer-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/sarah-ek/faer-rs/commits) --- updated-dependencies: - dependency-name: faer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9745b67b7405..d00620630e79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,9 +345,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "faer" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8894ad9275f1fd708e2f114f4059e80057ad3cc150b77a3f8a7991dd47ab37" +checksum = "0821d176d7fd17ea91d8a5ce84c56413e11410f404e418bfd205acf40184d819" dependencies = [ "bytemuck", "coe-rs", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index f444a5c8ed66..4b5c18e5eb93 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2" num-complex.workspace = true rustworkx-core.workspace = true num-bigint.workspace = true -faer = "0.19.2" +faer = "0.19.3" itertools.workspace = true qiskit-circuit.workspace = true thiserror.workspace = true From 3d4bab2be7fd09f6dcf734a3386355cae6cded39 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:26:48 -0400 Subject: [PATCH 06/11] Add method to add instructions to a DAGCircuit from an iterator of PackedInstruction (#13032) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial: Add add_from_iter method to DAGCircuit - Introduce a method that adds a chain of `PackedInstruction` continuously avoiding the re-linking of each bit's output-node until the very end of the iterator. - TODO: Add handling of vars - Add header for a `from_iter` function that will create a `DAGCircuit` based on a chain of `PackedInstruction`. * Fix: leverage new methods in layers - Fix incorrect re-insertion of last_node. * Fix: Keep track of Vars for add_from_iter - Remove `from_iter` * Fix: Incorrect modification of last nodes in `add_from_iter`. - Use `entry` api to either modify or insert a value if missing. * Fix: Cycling edges in when adding vars. - A bug that adds duplicate edges to vars has been temporarily fixed. However, the root of this problem hasn't been found yet. A proper fix is pending. For now skip those instances. * Fix: Remove set collecting all nodes to be connected. - A set collecting all the new nodes to connect with a new node was preventing additional wires to connect to subsequent nodes. * Fix: Adapt to #13033 * Refactor: `add_from_iter` is now called `extend` to stick with `Rust` nomenclature. * Fix docstring - Caught by @ElePT Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Fix: Remove duplicate vars check * Fix: Corrections from code review. - Use Entry API to modify last nodes in the var. - Build new_nodes with an allocated vec. - Add comment explaining the removal of the edge between the output node and its predecessor. * Fix: Improper use of `Entry API`. - Use `or_insert_with` instead of `or_insert` to perform actions before inserting a value. --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/circuit/src/dag_circuit.rs | 141 +++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 3 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 32b3a77ed24c..4d41660708d3 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -4350,9 +4350,7 @@ def _format(operand): let mut new_layer = self.copy_empty_like(py, vars_mode)?; - for (node, _) in op_nodes { - new_layer.push_back(py, node.clone())?; - } + new_layer.extend(py, op_nodes.iter().map(|(inst, _)| (*inst).clone()))?; let new_layer_op_nodes = new_layer.op_nodes(false).filter_map(|node_index| { match new_layer.dag.node_weight(node_index) { @@ -6366,6 +6364,143 @@ impl DAGCircuit { Err(DAGCircuitError::new_err("Specified node is not an op node")) } } + + /// Extends the DAG with valid instances of [PackedInstruction] + pub fn extend(&mut self, py: Python, iter: I) -> PyResult> + where + I: IntoIterator, + { + // Create HashSets to keep track of each bit/var's last node + let mut qubit_last_nodes: HashMap = HashMap::default(); + let mut clbit_last_nodes: HashMap = HashMap::default(); + // TODO: Refactor once Vars are in rust + // Dict [ Var: (int, VarWeight)] + let vars_last_nodes: Bound = PyDict::new_bound(py); + + // Consume into iterator to obtain size hint + let iter = iter.into_iter(); + // Store new nodes to return + let mut new_nodes = Vec::with_capacity(iter.size_hint().1.unwrap_or_default()); + for instr in iter { + let op_name = instr.op.name(); + let (all_cbits, vars): (Vec, Option>) = { + if self.may_have_additional_wires(py, &instr) { + let mut clbits: HashSet = + HashSet::from_iter(self.cargs_interner.get(instr.clbits).iter().copied()); + let (additional_clbits, additional_vars) = + self.additional_wires(py, instr.op.view(), instr.condition())?; + for clbit in additional_clbits { + clbits.insert(clbit); + } + (clbits.into_iter().collect(), Some(additional_vars)) + } else { + (self.cargs_interner.get(instr.clbits).to_vec(), None) + } + }; + + // Increment the operation count + self.increment_op(op_name); + + // Get the correct qubit indices + let qubits_id = instr.qubits; + + // Insert op-node to graph. + let new_node = self.dag.add_node(NodeType::Operation(instr)); + new_nodes.push(new_node); + + // Check all the qubits in this instruction. + for qubit in self.qargs_interner.get(qubits_id) { + // Retrieve each qubit's last node + let qubit_last_node = *qubit_last_nodes.entry(*qubit).or_insert_with(|| { + // If the qubit is not in the last nodes collection, the edge between the output node and its predecessor. + // Then, store the predecessor's NodeIndex in the last nodes collection. + let output_node = self.qubit_io_map[qubit.0 as usize][1]; + let (edge_id, predecessor_node) = self + .dag + .edges_directed(output_node, Incoming) + .next() + .map(|edge| (edge.id(), edge.source())) + .unwrap(); + self.dag.remove_edge(edge_id); + predecessor_node + }); + qubit_last_nodes + .entry(*qubit) + .and_modify(|val| *val = new_node); + self.dag + .add_edge(qubit_last_node, new_node, Wire::Qubit(*qubit)); + } + + // Check all the clbits in this instruction. + for clbit in all_cbits { + let clbit_last_node = *clbit_last_nodes.entry(clbit).or_insert_with(|| { + // If the qubit is not in the last nodes collection, the edge between the output node and its predecessor. + // Then, store the predecessor's NodeIndex in the last nodes collection. + let output_node = self.clbit_io_map[clbit.0 as usize][1]; + let (edge_id, predecessor_node) = self + .dag + .edges_directed(output_node, Incoming) + .next() + .map(|edge| (edge.id(), edge.source())) + .unwrap(); + self.dag.remove_edge(edge_id); + predecessor_node + }); + clbit_last_nodes + .entry(clbit) + .and_modify(|val| *val = new_node); + self.dag + .add_edge(clbit_last_node, new_node, Wire::Clbit(clbit)); + } + + // If available, check all the vars in this instruction + for var in vars.iter().flatten() { + let var_last_node = if let Some(result) = vars_last_nodes.get_item(var)? { + let node: usize = result.extract()?; + vars_last_nodes.del_item(var)?; + NodeIndex::new(node) + } else { + // If the var is not in the last nodes collection, the edge between the output node and its predecessor. + // Then, store the predecessor's NodeIndex in the last nodes collection. + let output_node = self.var_output_map.get(py, var).unwrap(); + let (edge_id, predecessor_node) = self + .dag + .edges_directed(output_node, Incoming) + .next() + .map(|edge| (edge.id(), edge.source())) + .unwrap(); + self.dag.remove_edge(edge_id); + predecessor_node + }; + + vars_last_nodes.set_item(var, new_node.index())?; + self.dag + .add_edge(var_last_node, new_node, Wire::Var(var.clone_ref(py))); + } + } + + // Add the output_nodes back to qargs + for (qubit, node) in qubit_last_nodes { + let output_node = self.qubit_io_map[qubit.0 as usize][1]; + self.dag.add_edge(node, output_node, Wire::Qubit(qubit)); + } + + // Add the output_nodes back to cargs + for (clbit, node) in clbit_last_nodes { + let output_node = self.clbit_io_map[clbit.0 as usize][1]; + self.dag.add_edge(node, output_node, Wire::Clbit(clbit)); + } + + // Add the output_nodes back to vars + for item in vars_last_nodes.items() { + let (var, node): (PyObject, usize) = item.extract()?; + let output_node = self.var_output_map.get(py, &var).unwrap(); + self.dag + .add_edge(NodeIndex::new(node), output_node, Wire::Var(var)); + } + + Ok(new_nodes) + } } /// Add to global phase. Global phase can only be Float or ParameterExpression so this From 41dc41888ec60a7aab9b93aaa0aaebb5d4711a77 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 6 Sep 2024 16:52:42 -0400 Subject: [PATCH 07/11] Fully port CheckMap to Rust (#13030) * Fully port CheckMap to Rust This commit migrates the entirety of the CheckMap analysis pass to Rust. The pass operates solely in the rust domain and returns an `Option<(String, [u32; 2])>` to Python which is used to set the two property set fields appropriately. All the analysis of the dag is done in Rust. There is still Python interaction required though because control flow operations are only defined in Python. However the interaction is minimal and only to get the circuits for control flow blocks and converting them into DAGs (at least until #13001 is complete). This commit is based on top of #12959 and will need to be rebased after that merges. Closes #12251 Part of #12208 * Use a Vec for wire_map instead of a HashMap This commit switches to using a Vec for the internal wire_map used to map control flow qubits. A HashMap was originally used because in Python a dictionary is used. However, in the rust domain the inner qubits are contiguous integers starting from 0 so a Vec can be used for better performance in the case we have control flow. * Update crates/accelerate/src/check_map.rs Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --------- Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/check_map.rs | 98 +++++++++++++++++++++ crates/accelerate/src/lib.rs | 1 + crates/pyext/src/lib.rs | 13 +-- qiskit/__init__.py | 1 + qiskit/transpiler/passes/utils/check_map.py | 33 +++---- 5 files changed, 117 insertions(+), 29 deletions(-) create mode 100644 crates/accelerate/src/check_map.rs diff --git a/crates/accelerate/src/check_map.rs b/crates/accelerate/src/check_map.rs new file mode 100644 index 000000000000..8ebf50cd372d --- /dev/null +++ b/crates/accelerate/src/check_map.rs @@ -0,0 +1,98 @@ +// 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::HashSet; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::imports::CIRCUIT_TO_DAG; +use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::Qubit; + +fn recurse<'py>( + py: Python<'py>, + dag: &'py DAGCircuit, + edge_set: &'py HashSet<[u32; 2]>, + wire_map: Option<&'py [Qubit]>, +) -> PyResult> { + let check_qubits = |qubits: &[Qubit]| -> bool { + match wire_map { + Some(wire_map) => { + let mapped_bits = [ + wire_map[qubits[0].0 as usize], + wire_map[qubits[1].0 as usize], + ]; + edge_set.contains(&[mapped_bits[0].into(), mapped_bits[1].into()]) + } + None => edge_set.contains(&[qubits[0].into(), qubits[1].into()]), + } + }; + for node in dag.op_nodes(false) { + if let NodeType::Operation(inst) = &dag.dag[node] { + let qubits = dag.get_qargs(inst.qubits); + if inst.op.control_flow() { + if let OperationRef::Instruction(py_inst) = inst.op.view() { + let raw_blocks = py_inst.instruction.getattr(py, "blocks")?; + let circuit_to_dag = CIRCUIT_TO_DAG.get_bound(py); + for raw_block in raw_blocks.bind(py).iter().unwrap() { + let block_obj = raw_block?; + let block = block_obj + .getattr(intern!(py, "_data"))? + .downcast::()? + .borrow(); + let new_dag: DAGCircuit = + circuit_to_dag.call1((block_obj.clone(),))?.extract()?; + let wire_map = (0..block.num_qubits()) + .map(|inner| { + let outer = qubits[inner]; + match wire_map { + Some(wire_map) => wire_map[outer.0 as usize], + None => outer, + } + }) + .collect::>(); + let res = recurse(py, &new_dag, edge_set, Some(&wire_map))?; + if res.is_some() { + return Ok(res); + } + } + } + } else if qubits.len() == 2 + && (dag.calibrations_empty() || !dag.has_calibration_for_index(py, node)?) + && !check_qubits(qubits) + { + return Ok(Some(( + inst.op.name().to_string(), + [qubits[0].0, qubits[1].0], + ))); + } + } + } + Ok(None) +} + +#[pyfunction] +pub fn check_map( + py: Python, + dag: &DAGCircuit, + edge_set: HashSet<[u32; 2]>, +) -> PyResult> { + recurse(py, dag, &edge_set, None) +} + +pub fn check_map_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(check_map))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 78eea97faad0..f11f28071e2c 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -14,6 +14,7 @@ use std::env; use pyo3::import_exception; +pub mod check_map; pub mod circuit_library; pub mod commutation_analysis; pub mod commutation_checker; diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 49e44bffa2ec..ec883c69c3f1 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -13,12 +13,12 @@ use pyo3::prelude::*; use qiskit_accelerate::{ - circuit_library::circuit_library, commutation_analysis::commutation_analysis, - commutation_checker::commutation_checker, convert_2q_block_matrix::convert_2q_block_matrix, - dense_layout::dense_layout, error_map::error_map, - euler_one_qubit_decomposer::euler_one_qubit_decomposer, filter_op_nodes::filter_op_nodes_mod, - isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, - pauli_exp_val::pauli_expval, + check_map::check_map_mod, circuit_library::circuit_library, + commutation_analysis::commutation_analysis, commutation_checker::commutation_checker, + convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, + error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, + filter_op_nodes::filter_op_nodes_mod, isometry::isometry, nlayout::nlayout, + optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, 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, @@ -43,6 +43,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, qiskit_qasm2::qasm2, "qasm2")?; add_submodule(m, qiskit_qasm3::qasm3, "qasm3")?; add_submodule(m, circuit_library, "circuit_library")?; + add_submodule(m, check_map_mod, "check_map")?; add_submodule(m, convert_2q_block_matrix, "convert_2q_block_matrix")?; add_submodule(m, dense_layout, "dense_layout")?; add_submodule(m, error_map, "error_map")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 3cc10bf96a31..386b9cd082da 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -92,6 +92,7 @@ sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index bd78c65de5f4..4048d93df22c 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -14,7 +14,8 @@ from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.target import Target -from qiskit.converters import circuit_to_dag + +from qiskit._accelerate import check_map class CheckMap(AnalysisPass): @@ -67,25 +68,11 @@ def run(self, dag): if not self.qargs: self.property_set[self.property_set_field] = True return - wire_map = {bit: index for index, bit in enumerate(dag.qubits)} - self.property_set[self.property_set_field] = self._recurse(dag, wire_map) - - def _recurse(self, dag, wire_map) -> bool: - for node in dag.op_nodes(include_directives=False): - if node.is_control_flow(): - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for inner, outer in zip(block.qubits, node.qargs) - } - if not self._recurse(circuit_to_dag(block), inner_wire_map): - return False - elif ( - len(node.qargs) == 2 - and not dag.has_calibration_for(node) - and (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) not in self.qargs - ): - self.property_set["check_map_msg"] = ( - f"{node.name}({wire_map[node.qargs[0]]}, {wire_map[node.qargs[1]]}) failed" - ) - return False - return True + res = check_map.check_map(dag, self.qargs) + if res is None: + self.property_set[self.property_set_field] = True + return + self.property_set[self.property_set_field] = False + self.property_set["check_map_msg"] = ( + f"{res[0]}({dag.qubits[res[1][0]]}, {dag.qubits[res[1][1]]}) failed" + ) From 94ba19e9fbbbdc0176b235e907354c0fab7468f5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 6 Sep 2024 19:08:47 -0400 Subject: [PATCH 08/11] Fully port InverseCancellation to Rust (#13013) * Fully port InverseCancellation to Rust This commit builds off of #12959 and the other data model in Rust infrastructure and migrates the InverseCancellation pass to operate fully in Rust. The full path of the transpiler pass now never leaves Rust until it has finished modifying the DAGCircuit. There is still some python interaction necessary to handle parts of the data model that are still in Python, mainly for handling parameter expressions. But otherwise the entirety of the pass operates in rust now. This is just a first pass at the migration here, it moves the pass to use loops in rust. The next steps here are to look at operating the pass in parallel. There is no data dependency between the optimizations being done for different inverse gates/pairs so we should be able to the throughput of the pass by leveraging multithreading to handle each inverse option in parallel. This commit does not attempt this though, because of the Python dependency and also the data structures around gates and the dag aren't really setup for multithreading yet and there likely will need to be some work to support that. Fixes #12271 Part of #12208 * Remove temporary variable for chunk empty check * Destructure gate pairs * Rework short circuit logic --- crates/accelerate/src/inverse_cancellation.rs | 191 ++++++++++++++++++ crates/accelerate/src/lib.rs | 1 + crates/circuit/src/dag_circuit.rs | 94 +++++---- crates/pyext/src/lib.rs | 6 +- qiskit/__init__.py | 1 + .../optimization/inverse_cancellation.py | 101 +-------- 6 files changed, 260 insertions(+), 134 deletions(-) create mode 100644 crates/accelerate/src/inverse_cancellation.rs diff --git a/crates/accelerate/src/inverse_cancellation.rs b/crates/accelerate/src/inverse_cancellation.rs new file mode 100644 index 000000000000..0b3404c92ffb --- /dev/null +++ b/crates/accelerate/src/inverse_cancellation.rs @@ -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 { + 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 { + 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 { + 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 [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 = [&gate_0, &gate_1] + .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, &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, + inverse_gate_names: HashSet, + self_inverse_gate_names: HashSet, +) -> 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) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(inverse_cancellation))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index f11f28071e2c..26d93ab3f65e 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -24,6 +24,7 @@ pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; pub mod filter_op_nodes; +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 4d41660708d3..e57942f87cc0 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -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 { - 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. @@ -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> { + 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) + } + } + /// Extends the DAG with valid instances of [PackedInstruction] pub fn extend(&mut self, py: Python, iter: I) -> PyResult> where diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index ec883c69c3f1..331703809397 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -17,8 +17,9 @@ use qiskit_accelerate::{ commutation_analysis::commutation_analysis, commutation_checker::commutation_checker, convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, - filter_op_nodes::filter_op_nodes_mod, isometry::isometry, nlayout::nlayout, - optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, + filter_op_nodes::filter_op_nodes_mod, inverse_cancellation::inverse_cancellation_mod, + isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, + pauli_exp_val::pauli_expval, remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, 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, @@ -48,6 +49,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, filter_op_nodes_mod, "filter_op_nodes")?; add_submodule(m, isometry, "isometry")?; add_submodule(m, nlayout, "nlayout")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 386b9cd082da..27830243d852 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -92,6 +92,7 @@ sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes 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 From 734560e6c922afb88f2d4796e611e16a2d7923fa Mon Sep 17 00:00:00 2001 From: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:10:30 +0300 Subject: [PATCH 09/11] Oxidize CheckGateDirection (#13042) * First working implementation * A control flow fix, lint, doc and some more polishing * Addressing review comments and further simplifying the code * Address more review comments * Use explicit temp storage for mapping to avoid E0716 in Rust 1.70 * Allow checking 2Q non-control flow instructions --- crates/accelerate/src/gate_direction.rs | 150 ++++++++++++++++++ crates/accelerate/src/lib.rs | 1 + .../accelerate/src/target_transpiler/mod.rs | 2 +- crates/circuit/src/dag_circuit.rs | 30 ++-- crates/circuit/src/lib.rs | 2 +- crates/pyext/src/lib.rs | 7 +- qiskit/__init__.py | 1 + .../passes/utils/check_gate_direction.py | 49 +----- .../transpiler/test_check_gate_direction.py | 26 +-- 9 files changed, 196 insertions(+), 72 deletions(-) create mode 100644 crates/accelerate/src/gate_direction.rs diff --git a/crates/accelerate/src/gate_direction.rs b/crates/accelerate/src/gate_direction.rs new file mode 100644 index 000000000000..aee5ca097322 --- /dev/null +++ b/crates/accelerate/src/gate_direction.rs @@ -0,0 +1,150 @@ +// 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 crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use hashbrown::HashSet; +use pyo3::prelude::*; +use qiskit_circuit::imports; +use qiskit_circuit::operations::OperationRef; +use qiskit_circuit::{ + dag_circuit::{DAGCircuit, NodeType}, + operations::Operation, + packed_instruction::PackedInstruction, + Qubit, +}; +use smallvec::smallvec; + +/// Check if the two-qubit gates follow the right direction with respect to the coupling map. +/// +/// Args: +/// dag: the DAGCircuit to analyze +/// +/// coupling_edges: set of edge pairs representing a directed coupling map, against which gate directionality is checked +/// +/// Returns: +/// true iff all two-qubit gates comply with the coupling constraints +#[pyfunction] +#[pyo3(name = "check_gate_direction_coupling")] +fn py_check_with_coupling_map( + py: Python, + dag: &DAGCircuit, + coupling_edges: HashSet<[Qubit; 2]>, +) -> PyResult { + let coupling_map_check = + |_: &PackedInstruction, op_args: &[Qubit]| -> bool { coupling_edges.contains(op_args) }; + + check_gate_direction(py, dag, &coupling_map_check, None) +} + +/// Check if the two-qubit gates follow the right direction with respect to instructions supported in the given target. +/// +/// Args: +/// dag: the DAGCircuit to analyze +/// +/// target: the Target against which gate directionality compliance is checked +/// +/// Returns: +/// true iff all two-qubit gates comply with the target's coupling constraints +#[pyfunction] +#[pyo3(name = "check_gate_direction_target")] +fn py_check_with_target(py: Python, dag: &DAGCircuit, target: &Target) -> PyResult { + let target_check = |inst: &PackedInstruction, op_args: &[Qubit]| -> bool { + let qargs = smallvec![ + PhysicalQubit::new(op_args[0].0), + PhysicalQubit::new(op_args[1].0) + ]; + + target.instruction_supported(inst.op.name(), Some(&qargs)) + }; + + check_gate_direction(py, dag, &target_check, None) +} + +// The main routine for checking gate directionality. +// +// gate_complies: a function returning true iff the two-qubit gate direction complies with directionality constraints +// +// qubit_mapping: used for mapping the index of a given qubit within an instruction qargs vector to the corresponding qubit index of the +// original DAGCircuit the pass was called with. This mapping is required since control flow blocks are represented by nested DAGCircuit +// objects whose instruction qubit indices are relative to the parent DAGCircuit they reside in, thus when we recurse into nested DAGs, we need +// to carry the mapping context relative to the original DAG. +// When qubit_mapping is None, the identity mapping is assumed +fn check_gate_direction( + py: Python, + dag: &DAGCircuit, + gate_complies: &T, + qubit_mapping: Option<&[Qubit]>, +) -> PyResult +where + T: Fn(&PackedInstruction, &[Qubit]) -> bool, +{ + for node in dag.op_nodes(false) { + let NodeType::Operation(packed_inst) = &dag.dag[node] else { + panic!("PackedInstruction is expected"); + }; + + let inst_qargs = dag.get_qargs(packed_inst.qubits); + + if let OperationRef::Instruction(py_inst) = packed_inst.op.view() { + if py_inst.control_flow() { + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); // TODO: Take out of the recursion + let py_inst = py_inst.instruction.bind(py); + + for block in py_inst.getattr("blocks")?.iter()? { + let inner_dag: DAGCircuit = circuit_to_dag.call1((block?,))?.extract()?; + + let block_ok = if let Some(mapping) = qubit_mapping { + let mapping = inst_qargs // Create a temp mapping for the recursive call + .iter() + .map(|q| mapping[q.0 as usize]) + .collect::>(); + + check_gate_direction(py, &inner_dag, gate_complies, Some(&mapping))? + } else { + check_gate_direction(py, &inner_dag, gate_complies, Some(inst_qargs))? + }; + + if !block_ok { + return Ok(false); + } + } + continue; + } + } + + if inst_qargs.len() == 2 + && !match qubit_mapping { + // Check gate direction based either on a given custom mapping or the identity mapping + Some(mapping) => gate_complies( + packed_inst, + &[ + mapping[inst_qargs[0].0 as usize], + mapping[inst_qargs[1].0 as usize], + ], + ), + None => gate_complies(packed_inst, inst_qargs), + } + { + return Ok(false); + } + } + + Ok(true) +} + +#[pymodule] +pub fn gate_direction(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(py_check_with_coupling_map))?; + m.add_wrapped(wrap_pyfunction!(py_check_with_target))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 26d93ab3f65e..916aeeb8351c 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -24,6 +24,7 @@ pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; pub mod filter_op_nodes; +pub mod gate_direction; pub mod inverse_cancellation; pub mod isometry; pub mod nlayout; diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 08b3e90eabba..65fc8e80d750 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -50,7 +50,7 @@ mod exceptions { } // Custom types -type Qargs = SmallVec<[PhysicalQubit; 2]>; +pub type Qargs = SmallVec<[PhysicalQubit; 2]>; type GateMap = IndexMap; type PropsMap = NullableIndexMap>; type GateMapState = Vec<(String, Vec<(Option, Option)>)>; diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index e57942f87cc0..5b9769b4d19e 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -3975,19 +3975,12 @@ def _format(operand): } /// Get list of 2 qubit operations. Ignore directives like snapshot and barrier. - fn two_qubit_ops(&self, py: Python) -> PyResult>> { + #[pyo3(name = "two_qubit_ops")] + pub fn py_two_qubit_ops(&self, py: Python) -> PyResult>> { let mut nodes = Vec::new(); - for (node, weight) in self.dag.node_references() { - if let NodeType::Operation(ref packed) = weight { - if packed.op.directive() { - continue; - } - - let qargs = self.qargs_interner.get(packed.qubits); - if qargs.len() == 2 { - nodes.push(self.unpack_into(py, node, weight)?); - } - } + for node in self.two_qubit_ops() { + let weight = self.dag.node_weight(node).expect("NodeIndex in graph"); + nodes.push(self.unpack_into(py, node, weight)?); } Ok(nodes) } @@ -5756,6 +5749,19 @@ impl DAGCircuit { } } + /// Return an iterator of 2 qubit operations. Ignore directives like snapshot and barrier. + pub fn two_qubit_ops(&self) -> impl Iterator + '_ { + Box::new(self.op_nodes(false).filter(|index| { + let weight = self.dag.node_weight(*index).expect("NodeIndex in graph"); + if let NodeType::Operation(ref packed) = weight { + let qargs = self.qargs_interner.get(packed.qubits); + qargs.len() == 2 + } else { + false + } + })) + } + // Filter any nodes that don't match a given predicate function pub fn filter_op_nodes(&mut self, mut predicate: F) where diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 4ca86c2ca83c..5106ba030288 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -32,7 +32,7 @@ use pyo3::prelude::*; use pyo3::types::{PySequence, PyTuple}; pub type BitType = u32; -#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq, FromPyObject)] pub struct Qubit(pub BitType); #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] pub struct Clbit(pub BitType); diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 331703809397..96b5e84a9cbe 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -17,9 +17,9 @@ use qiskit_accelerate::{ commutation_analysis::commutation_analysis, commutation_checker::commutation_checker, convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, - filter_op_nodes::filter_op_nodes_mod, inverse_cancellation::inverse_cancellation_mod, - isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, - pauli_exp_val::pauli_expval, + filter_op_nodes::filter_op_nodes_mod, gate_direction::gate_direction, + inverse_cancellation::inverse_cancellation_mod, isometry::isometry, nlayout::nlayout, + optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, 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, @@ -72,6 +72,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, uc_gate, "uc_gate")?; add_submodule(m, utils, "utils")?; add_submodule(m, vf2_layout, "vf2_layout")?; + add_submodule(m, gate_direction, "gate_direction")?; add_submodule(m, commutation_checker, "commutation_checker")?; add_submodule(m, commutation_analysis, "commutation_analysis")?; Ok(()) diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 27830243d852..f731465beecb 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -92,6 +92,7 @@ sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.gate_direction"] = _accelerate.gate_direction sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes diff --git a/qiskit/transpiler/passes/utils/check_gate_direction.py b/qiskit/transpiler/passes/utils/check_gate_direction.py index e797be95c4a1..5251bfc8de96 100644 --- a/qiskit/transpiler/passes/utils/check_gate_direction.py +++ b/qiskit/transpiler/passes/utils/check_gate_direction.py @@ -12,9 +12,11 @@ """Check if the gates follow the right direction with respect to the coupling map.""" -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import AnalysisPass +from qiskit._accelerate.gate_direction import ( + check_gate_direction_coupling, + check_gate_direction_target, +) class CheckGateDirection(AnalysisPass): @@ -34,42 +36,6 @@ def __init__(self, coupling_map, target=None): self.coupling_map = coupling_map self.target = target - def _coupling_map_visit(self, dag, wire_map, edges=None): - if edges is None: - edges = self.coupling_map.get_edges() - # Don't include directives to avoid things like barrier, which are assumed always supported. - for node in dag.op_nodes(include_directives=False): - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) - } - - if not self._coupling_map_visit(circuit_to_dag(block), inner_wire_map, edges): - return False - elif ( - len(node.qargs) == 2 - and (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) not in edges - ): - return False - return True - - def _target_visit(self, dag, wire_map): - # Don't include directives to avoid things like barrier, which are assumed always supported. - for node in dag.op_nodes(include_directives=False): - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) - } - if not self._target_visit(circuit_to_dag(block), inner_wire_map): - return False - elif len(node.qargs) == 2 and not self.target.instruction_supported( - node.name, (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) - ): - return False - return True - def run(self, dag): """Run the CheckGateDirection pass on `dag`. @@ -79,9 +45,8 @@ def run(self, dag): Args: dag (DAGCircuit): DAG to check. """ - wire_map = {bit: i for i, bit in enumerate(dag.qubits)} self.property_set["is_direction_mapped"] = ( - self._coupling_map_visit(dag, wire_map) - if self.target is None - else self._target_visit(dag, wire_map) + check_gate_direction_target(dag, self.target) + if self.target + else check_gate_direction_coupling(dag, set(self.coupling_map.get_edges())) ) diff --git a/test/python/transpiler/test_check_gate_direction.py b/test/python/transpiler/test_check_gate_direction.py index 0743f4c04cfc..efd38af16634 100644 --- a/test/python/transpiler/test_check_gate_direction.py +++ b/test/python/transpiler/test_check_gate_direction.py @@ -72,13 +72,13 @@ def test_true_direction(self): def test_true_direction_in_same_layer(self): """Two CXs distance_qubits 1 to each other, in the same layer - qr0:--(+)-- + qr0:---.-- | - qr1:---.--- + qr1:--(+)--- - qr2:--(+)-- + qr2:---.--- | - qr3:---.--- + qr3:--(+)-- CouplingMap map: [0]->[1]->[2]->[3] """ @@ -96,9 +96,9 @@ def test_true_direction_in_same_layer(self): def test_wrongly_mapped(self): """Needs [0]-[1] in a [0]--[2]--[1] - qr0:--(+)-- + qr0:---.--- | - qr1:---.--- + qr1:--(+)-- CouplingMap map: [0]->[2]->[1] """ @@ -115,11 +115,11 @@ def test_wrongly_mapped(self): def test_true_direction_undirected(self): """Mapped but with wrong direction - qr0:--(+)-[H]--.-- + qr0:---.--[H]-(+)- | | - qr1:---.-------|-- + qr1:--(+)------|-- | - qr2:----------(+)- + qr2:-----------.-- CouplingMap map: [1]<-[0]->[2] """ @@ -138,13 +138,13 @@ def test_true_direction_undirected(self): def test_false_direction_in_same_layer_undirected(self): """Two CXs in the same layer, but one is wrongly directed - qr0:--(+)-- + qr0:---.--- | - qr1:---.--- + qr1:--(+)-- - qr2:---.--- + qr2:--(+)-- | - qr3:--(+)-- + qr3:---.--- CouplingMap map: [0]->[1]->[2]->[3] """ From 3aa58cc2ab8bc0c4e600dbfe9c01c6ce4872ec0d Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 9 Sep 2024 09:49:41 +0100 Subject: [PATCH 10/11] Add equivalence-library rules between `rzz` and `cp` (#13019) * Add equivalence-library rules between `rzz` and `cp` These gates are locally equivalence (as are all the Ising-interaction gates), and this simple additional rule lets things like QFT, which are defined by Qiskit's default constructor in terms of `cp`, get converted into `rzz` or `rzx`. One `ControlledGate` test needed a case removing, because the underlying control mechanism now works correctly. * Rewrite release note --- .../standard_gates/equivalence_library.py | 43 +++++++++++++++++++ ...hase-rzz-equivalence-e8afc37b71a74366.yaml | 17 ++++++++ test/python/circuit/test_controlled_gate.py | 1 - 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index c4619ca27858..93e772ff842d 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -189,6 +189,21 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): cphase_to_cu1.append(CU1Gate(theta), [0, 1]) _sel.add_equivalence(CPhaseGate(theta), cphase_to_cu1) +# CPhaseGate +# +# global phase: ϴ/4 +# ┌─────────┐ +# q_0: ─■──── q_0: ─■─────────┤ Rz(ϴ/2) ├ +# │P(ϴ) ≡ │ZZ(-ϴ/2) ├─────────┤ +# q_1: ─■──── q_1: ─■─────────┤ Rz(ϴ/2) ├ +# └─────────┘ +theta = Parameter("theta") +cphase_to_rzz = QuantumCircuit(2, global_phase=theta / 4) +cphase_to_rzz.rzz(-theta / 2, 0, 1) +cphase_to_rzz.rz(theta / 2, 0) +cphase_to_rzz.rz(theta / 2, 1) +_sel.add_equivalence(CPhaseGate(theta), cphase_to_rzz) + # RGate # # ┌────────┐ ┌───────────────────────┐ @@ -394,6 +409,19 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): def_rzx.append(inst, qargs, cargs) _sel.add_equivalence(RZXGate(theta), def_rzx) +# RZXGate to RZZGate +# ┌─────────┐ +# q_0: ┤0 ├ q_0: ──────■─────────── +# │ Rzx(ϴ) │ ≡ ┌───┐ │ZZ(ϴ) ┌───┐ +# q_1: ┤1 ├ q_1: ┤ H ├─■──────┤ H ├ +# └─────────┘ └───┘ └───┘ +theta = Parameter("theta") +rzx_to_rzz = QuantumCircuit(2) +rzx_to_rzz.h(1) +rzx_to_rzz.rzz(theta, 0, 1) +rzx_to_rzz.h(1) +_sel.add_equivalence(RZXGate(theta), rzx_to_rzz) + # RYGate # @@ -654,6 +682,21 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): rzz_to_rzx.h(1) _sel.add_equivalence(RZZGate(theta), rzz_to_rzx) +# RZZ to CPhase +# +# global phase: ϴ/2 +# ┌───────┐ +# q_0: ─■───── q_0: ─■────────┤ Rz(ϴ) ├ +# │ZZ(ϴ) ≡ │P(-2*ϴ) ├───────┤ +# q_1: ─■───── q_1: ─■────────┤ Rz(ϴ) ├ +# └───────┘ +theta = Parameter("theta") +rzz_to_cphase = QuantumCircuit(2, global_phase=theta / 2) +rzz_to_cphase.cp(-theta * 2, 0, 1) +rzz_to_cphase.rz(theta, 0) +rzz_to_cphase.rz(theta, 1) +_sel.add_equivalence(RZZGate(theta), rzz_to_cphase) + # RZZ to RYY q = QuantumRegister(2, "q") theta = Parameter("theta") diff --git a/releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml b/releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml new file mode 100644 index 000000000000..9f4d9d384172 --- /dev/null +++ b/releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml @@ -0,0 +1,17 @@ +--- +features_circuits: + - | + The standard equivalence library (:data:`.SessionEquivalenceLibrary`) now has rules that can + directly convert between Qiskit's standard-library 2q continuous Ising-type interactions (e.g. + :class:`.CPhaseGate`, :class:`.RZZGate`, :class:`.RZXGate`, and so on) using local equivalence + relations. Previously, several of these conversions would go via a 2-CX form, which resulted + in less efficient circuit generation. + + .. note:: + + In general, the :class:`.BasisTranslator` is not guaranteed to find the "best" equivalence + relation for a given :class:`.Target`, but will always find an equivalence if one exists. We + rely on more expensive resynthesis and gate-optimization passes in the transpiler to improve + the output. These passes are currently not as effective for basis sets with a continuously + parametrized two-qubit interaction as they are for discrete super-controlled two-qubit + interactions. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index a517d5d1e4a4..03a041355b87 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -1438,7 +1438,6 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): @data( RXGate, RYGate, - RZGate, RXXGate, RYYGate, RZXGate, From 2ef371ae0d159a6dfd643805f3e5e5fdec37ab88 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 9 Sep 2024 09:09:24 -0400 Subject: [PATCH 11/11] Fully port Split2QUnitaries to rust (#13025) * Fully port Split2QUnitaries to rust This commit builds off of #13013 and the other data model in Rust infrastructure and migrates the InverseCancellation pass to operate fully in Rust. The full path of the transpiler pass now never leaves Rust until it has finished modifying the DAGCircuit. There is still some python interaction necessary to handle parts of the data model that are still in Python, mainly for creating `UnitaryGate` instances and `ParameterExpression` for global phase. But otherwise the entirety of the pass operates in rust now. This is just a first pass at the migration here, it moves the pass to use loops in rust. The next steps here are to look at operating the pass in parallel. There is no data dependency between the optimizations being done for different gates so we should be able to increase the throughput of the pass by leveraging multithreading to handle each gate in parallel. This commit does not attempt this though, because of the Python dependency and also the data structures around gates and the dag aren't really setup for multithreading yet and there likely will need to be some work to support that. Part of #12208 * Update pass logic with changes from #13095 Some of the logic inside the Split2QUnitaries pass was updated in a recently merged PR. This commit makes those changes so the rust implementation matches the current state of the previous python version. * Use op_nodes() instead of topological_op_nodes() * Use Fn trait instead of FnMut for callback We don't need the callback to be mutable currently so relax the trait to just be `Fn` instead of `FnMut`. If we have a need for a mutable environment callback in the future we can change this easily enough without any issues. * Avoid extra edge operations in replace_on_incoming_qubits * Rename function --- crates/accelerate/src/lib.rs | 1 + crates/accelerate/src/split_2q_unitaries.rs | 75 +++++++++++++++++++ crates/accelerate/src/two_qubit_decompose.rs | 12 +-- crates/circuit/src/dag_circuit.rs | 71 +++++++++++++++++- crates/circuit/src/imports.rs | 4 + crates/pyext/src/lib.rs | 8 +- qiskit/__init__.py | 1 + .../passes/optimization/split_2q_unitaries.py | 44 +---------- 8 files changed, 164 insertions(+), 52 deletions(-) create mode 100644 crates/accelerate/src/split_2q_unitaries.rs diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 916aeeb8351c..9e391e0a9030 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -35,6 +35,7 @@ pub mod results; pub mod sabre; pub mod sampled_exp_val; pub mod sparse_pauli_op; +pub mod split_2q_unitaries; pub mod star_prerouting; pub mod stochastic_swap; pub mod synthesis; diff --git a/crates/accelerate/src/split_2q_unitaries.rs b/crates/accelerate/src/split_2q_unitaries.rs new file mode 100644 index 000000000000..83ab94bf301d --- /dev/null +++ b/crates/accelerate/src/split_2q_unitaries.rs @@ -0,0 +1,75 @@ +// 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::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; +use qiskit_circuit::imports::UNITARY_GATE; +use qiskit_circuit::operations::{Operation, Param}; + +use crate::two_qubit_decompose::{Specialization, TwoQubitWeylDecomposition}; + +#[pyfunction] +pub fn split_2q_unitaries( + py: Python, + dag: &mut DAGCircuit, + requested_fidelity: f64, +) -> PyResult<()> { + let nodes: Vec = dag.op_nodes(false).collect(); + for node in nodes { + if let NodeType::Operation(inst) = &dag.dag[node] { + let qubits = dag.get_qargs(inst.qubits).to_vec(); + let matrix = inst.op.matrix(inst.params_view()); + // We only attempt to split UnitaryGate objects, but this could be extended in future + // -- however we need to ensure that we can compile the resulting single-qubit unitaries + // to the supported basis gate set. + if qubits.len() != 2 || inst.op.name() != "unitary" { + continue; + } + let decomp = TwoQubitWeylDecomposition::new_inner( + matrix.unwrap().view(), + Some(requested_fidelity), + None, + )?; + if matches!(decomp.specialization, Specialization::IdEquiv) { + let k1r_arr = decomp.K1r(py); + let k1l_arr = decomp.K1l(py); + let k1r_gate = UNITARY_GATE.get_bound(py).call1((k1r_arr,))?; + let k1l_gate = UNITARY_GATE.get_bound(py).call1((k1l_arr,))?; + let insert_fn = |edge: &Wire| -> PyResult { + if let Wire::Qubit(qubit) = edge { + if *qubit == qubits[0] { + k1r_gate.extract() + } else { + k1l_gate.extract() + } + } else { + unreachable!("This will only be called on ops with no classical wires."); + } + }; + dag.replace_node_with_1q_ops(py, node, insert_fn)?; + dag.add_global_phase(py, &Param::Float(decomp.global_phase))?; + } + // TODO: also look into splitting on Specialization::Swap and just + // swap the virtual qubits. Doing this we will need to update the + // permutation like in ElidePermutations + } + } + Ok(()) +} + +pub fn split_2q_unitaries_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(split_2q_unitaries))?; + Ok(()) +} diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 2c745b199e9e..92ad4724682f 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -341,7 +341,7 @@ const DEFAULT_FIDELITY: f64 = 1.0 - 1.0e-9; #[derive(Clone, Debug, Copy)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose")] -enum Specialization { +pub enum Specialization { General, IdEquiv, SWAPEquiv, @@ -410,13 +410,13 @@ pub struct TwoQubitWeylDecomposition { #[pyo3(get)] c: f64, #[pyo3(get)] - global_phase: f64, + pub global_phase: f64, K1l: Array2, K2l: Array2, K1r: Array2, K2r: Array2, #[pyo3(get)] - specialization: Specialization, + pub specialization: Specialization, default_euler_basis: EulerBasis, #[pyo3(get)] requested_fidelity: Option, @@ -476,7 +476,7 @@ impl TwoQubitWeylDecomposition { /// Instantiate a new TwoQubitWeylDecomposition with rust native /// data structures - fn new_inner( + pub fn new_inner( unitary_matrix: ArrayView2, fidelity: Option, @@ -1021,13 +1021,13 @@ impl TwoQubitWeylDecomposition { #[allow(non_snake_case)] #[getter] - fn K1l(&self, py: Python) -> PyObject { + pub fn K1l(&self, py: Python) -> PyObject { self.K1l.to_pyarray_bound(py).into() } #[allow(non_snake_case)] #[getter] - fn K1r(&self, py: Python) -> PyObject { + pub fn K1r(&self, py: Python) -> PyObject { self.K1r.to_pyarray_bound(py).into() } diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 5b9769b4d19e..0b5a43c1eb4b 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -5254,7 +5254,7 @@ impl DAGCircuit { Ok(nodes.into_iter()) } - fn topological_op_nodes(&self) -> PyResult + '_> { + pub fn topological_op_nodes(&self) -> PyResult + '_> { Ok(self.topological_nodes()?.filter(|node: &NodeIndex| { matches!(self.dag.node_weight(*node), Some(NodeType::Operation(_))) })) @@ -6285,6 +6285,75 @@ impl DAGCircuit { } } + /// Replace a node with individual operations from a provided callback + /// function on each qubit of that node. + #[allow(unused_variables)] + pub fn replace_node_with_1q_ops( + &mut self, + py: Python, // Unused if cache_pygates isn't enabled + node: NodeIndex, + insert: F, + ) -> PyResult<()> + where + F: Fn(&Wire) -> PyResult, + { + let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::with_capacity(2); + for (source, in_weight) in self + .dag + .edges_directed(node, Incoming) + .map(|x| (x.source(), x.weight())) + { + for (target, out_weight) in self + .dag + .edges_directed(node, Outgoing) + .map(|x| (x.target(), x.weight())) + { + if in_weight == out_weight { + edge_list.push((source, target, in_weight.clone())); + } + } + } + for (source, target, weight) in edge_list { + let new_op = insert(&weight)?; + self.increment_op(new_op.operation.name()); + let qubits = if let Wire::Qubit(qubit) = weight { + vec![qubit] + } else { + panic!("This method only works if the gate being replaced has no classical incident wires") + }; + #[cfg(feature = "cache_pygates")] + let py_op = match new_op.operation.view() { + OperationRef::Standard(_) => OnceCell::new(), + OperationRef::Gate(gate) => OnceCell::from(gate.gate.clone_ref(py)), + OperationRef::Instruction(instruction) => { + OnceCell::from(instruction.instruction.clone_ref(py)) + } + OperationRef::Operation(op) => OnceCell::from(op.operation.clone_ref(py)), + }; + let inst = PackedInstruction { + op: new_op.operation, + qubits: self.qargs_interner.insert_owned(qubits), + clbits: self.cargs_interner.get_default(), + params: (!new_op.params.is_empty()).then(|| Box::new(new_op.params)), + extra_attrs: new_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: py_op, + }; + let new_index = self.dag.add_node(NodeType::Operation(inst)); + self.dag.add_edge(source, new_index, weight.clone()); + self.dag.add_edge(new_index, target, weight); + } + + match self.dag.remove_node(node) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => panic!("Must be called with valid operation node"), + } + Ok(()) + } + pub fn add_global_phase(&mut self, py: Python, value: &Param) -> PyResult<()> { match value { Param::Obj(_) => { diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 588471546273..a8f6d9562559 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -109,6 +109,10 @@ pub static SWITCH_CASE_OP_CHECK: ImportOnceCell = pub static FOR_LOOP_OP_CHECK: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_for_loop_eq"); pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID"); +pub static UNITARY_GATE: ImportOnceCell = ImportOnceCell::new( + "qiskit.circuit.library.generalized_gates.unitary", + "UnitaryGate", +); /// A mapping from the enum variant in crate::operations::StandardGate to the python /// module path and class name to import it. This is used to populate the conversion table diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 96b5e84a9cbe..03bd0202dae4 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -22,9 +22,10 @@ use qiskit_accelerate::{ optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, 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, - target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, - utils::utils, vf2_layout::vf2_layout, + split_2q_unitaries::split_2q_unitaries_mod, star_prerouting::star_prerouting, + stochastic_swap::stochastic_swap, synthesis::synthesis, target_transpiler::target, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, + vf2_layout::vf2_layout, }; #[inline(always)] @@ -65,6 +66,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, sabre, "sabre")?; add_submodule(m, sampled_exp_val, "sampled_exp_val")?; add_submodule(m, sparse_pauli_op, "sparse_pauli_op")?; + add_submodule(m, split_2q_unitaries_mod, "split_2q_unitaries")?; add_submodule(m, star_prerouting, "star_prerouting")?; add_submodule(m, stochastic_swap, "stochastic_swap")?; add_submodule(m, target, "target")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index f731465beecb..29e2b8ba5c11 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -92,6 +92,7 @@ sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.split_2q_unitaries"] = _accelerate.split_2q_unitaries sys.modules["qiskit._accelerate.gate_direction"] = _accelerate.gate_direction sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 1ca53c6c31d2..f6958a00a4c1 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -13,11 +13,8 @@ """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" from qiskit.transpiler.basepasses import TransformationPass -from qiskit.circuit.quantumcircuitdata import CircuitInstruction from qiskit.dagcircuit.dagcircuit import DAGCircuit -from qiskit.dagcircuit.dagnode import DAGOpNode -from qiskit.circuit.library.generalized_gates import UnitaryGate -from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition +from qiskit._accelerate.split_2q_unitaries import split_2q_unitaries class Split2QUnitaries(TransformationPass): @@ -39,42 +36,5 @@ def __init__(self, fidelity: float = 1.0 - 1e-16): def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the Split2QUnitaries pass on `dag`.""" - - for node in dag.topological_op_nodes(): - # We only attempt to split UnitaryGate objects, but this could be extended in future - # -- however we need to ensure that we can compile the resulting single-qubit unitaries - # to the supported basis gate set. - if not (len(node.qargs) == 2 and node.op.name == "unitary"): - continue - - decomp = TwoQubitWeylDecomposition(node.matrix, fidelity=self.requested_fidelity) - if ( - decomp._inner_decomposition.specialization - == TwoQubitWeylDecomposition._specializations.IdEquiv - ): - new_dag = DAGCircuit() - new_dag.add_qubits(node.qargs) - - ur = decomp.K1r - ur_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)) - ) - - ul = decomp.K1l - ul_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)) - ) - new_dag._apply_op_node_back(ur_node) - new_dag._apply_op_node_back(ul_node) - new_dag.global_phase = decomp.global_phase - dag.substitute_node_with_dag(node, new_dag) - elif ( - decomp._inner_decomposition.specialization - == TwoQubitWeylDecomposition._specializations.SWAPEquiv - ): - # TODO maybe also look into swap-gate-like gates? Things to consider: - # * As the qubit mapping may change, we'll always need to build a new dag in this pass - # * There may not be many swap-gate-like gates in an arbitrary input circuit - # * Removing swap gates from a user-routed input circuit here is unexpected - pass + split_2q_unitaries(dag, self.requested_fidelity) return dag