From d3040a0b7f25d268c6342011d2d12d42354ed3bc Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 23 Aug 2024 12:40:59 -0400 Subject: [PATCH] [DAGCircuit Oxidation] Port `DAGCircuit` to Rust (#12550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Port DAGCircuit to Rust This commit migrates the entirety of the `DAGCircuit` class to Rust. It fully replaces the Python version of the class. The primary advantage of this migration is moving from a Python space rustworkx directed graph representation to a Rust space petgraph (the upstream library for rustworkx) directed graph. Moving the graph data structure to rust enables us to directly interact with the DAG directly from transpiler passes in Rust in the future. This will enable a significant speed-up in those transpiler passes. Additionally, this should also improve the memory footprint as the DAGCircuit no longer stores `DAGNode` instances, and instead stores a lighter enum NodeType, which simply contains a `PackedInstruction` or the wire objects directly. Internally, the new Rust-based `DAGCircuit` uses a `petgraph::StableGraph` with node weights of type `NodeType` and edge weights of type `Wire`. The NodeType enum contains variants for `QubitIn`, `QubitOut`, `ClbitIn`, `ClbitOut`, and `Operation`, which should save us from all of the `isinstance` checking previously needed when working with `DAGNode` Python instances. The `Wire` enum contains variants `Qubit`, `Clbit`, and `Var`. As the full Qiskit data model is not rust-native at this point while all the class code in the `DAGCircuit` exists in Rust now, there are still sections that rely on Python or actively run Python code via Rust to function. These typically involve anything that uses `condition`, control flow, classical vars, calibrations, bit/register manipulation, etc. In the future as we either migrate this functionality to Rust or deprecate and remove it this can be updated in place to avoid the use of Python. API access from Python-space remains in terms of `DAGNode` instances to maintain API compatibility with the Python implementation. However, internally, we convert to and deal in terms of NodeType. When the user requests a particular node via lookup or iteration, we inflate an ephemeral `DAGNode` based on the internal `NodeType` and give them that. This is very similar to what was done in #10827 when porting CircuitData to Rust. As part of this porting there are a few small differences to keep in mind with the new Rust implementation of DAGCircuit. The first is that the topological ordering is slightly different with the new DAGCircuit. Previously, the Python version of `DAGCircuit` using a lexicographical topological sort key which was basically `"0,1,0,2"` where the first `0,1` are qargs on qubit indices `0,1` for nodes and `0,2` are cargs on clbit indices `0,2`. However, the sort key has now changed to be `(&[Qubit(0), Qubit(1)], &[Clbit(0), Clbit(2)])` in rust in this case which for the most part should behave identically, but there are some edge cases that will appear where the sort order is different. It will always be a valid topological ordering as the lexicographical key is used as a tie breaker when generating a topological sort. But if you're relaying on the exact same sort order there will be differences after this PR. The second is that a lot of undocumented functionality in the DAGCircuit which previously worked because of Python's implicit support for interacting with data structures is no longer functional. For example, previously the `DAGCircuit.qubits` list could be set directly (as the circuit visualizers previously did), but this was never documented as supported (and would corrupt the DAGCircuit). Any functionality like this we'd have to explicit include in the Rust implementation and as they were not included in the documented public API this PR opted to remove the vast majority of this type of functionality. The last related thing might require future work to mitigate is that this PR breaks the linkage between `DAGNode` and the underlying `DAGCirucit` object. In the Python implementation the `DAGNode` objects were stored directly in the `DAGCircuit` and when an API method returned a `DAGNode` from the DAG it was a shared reference to the underlying object in the `DAGCircuit`. This meant if you mutated the `DAGNode` it would be reflected in the `DAGCircuit`. This was not always a sound usage of the API as the `DAGCircuit` was implicitly caching many attributes of the DAG and you should always be using the `DAGCircuit` API to mutate any nodes to prevent any corruption of the `DAGCircuit`. However, now as the underlying data store for nodes in the DAG are no longer the python space objects returned by `DAGCircuit` methods mutating a `DAGNode` will not make any change in the underlying `DAGCircuit`. This can come as quite the surprise at first, especially if you were relying on this side effect, even if it was unsound. It's also worth noting that 2 large pieces of functionality from rustworkx are included in this PR. These are the new files `rustworkx_core_vnext` and `dot_utils` which are rustworkx's VF2 implementation and its dot file generation. As there was not a rust interface exposed for this functionality from rustworkx-core there was no way to use these functions in rustworkx. Until these interfaces added to rustworkx-core in future releases we'll have to keep these local copies. The vf2 implementation is in progress in Qiskit/rustworkx#1235, but `dot_utils` might make sense to keep around longer term as it is slightly modified from the upstream rustworkx implementation to directly interface with `DAGCircuit` instead of a generic graph. Co-authored-by: Matthew Treinish Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Alexander Ivrii Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Co-authored-by: John Lapeyre Co-authored-by: Jake Lishman * Update visual mpl circuit drawer references Right now there is a bug in the matplotlib circuit visualizer likely caused by the new `__eq__` implementation for `DAGOpNode` that didn't exist before were some gates are missing from the visualization. In the interest of unblocking this PR this commit updates the references for these cases temporarily until this issue is fixed. * Ensure DAGNode.sort_key is always a string Previously the sort_key attribute of the Python space DAGCircuit was incorrectly being set to `None` for rust generated node objects. This was done as for the default path the sort key is determined from the rust domain's representation of qubits and there is no analogous data in the Python object. However, this was indavertandly a breaking API change as sort_key is expected to always be a string. This commit adds a default string to use for all node types so that we always have a reasonable value that matches the typing of the class. A future step is likely to add back the `dag` kwarg to the node types and generate the string on the fly from the rust space data. * Make Python argument first in Param::eq and Param::is_close The standard function signature convention for functions that take a `py: Python` argument is to make the Python argument the first (or second after `&self`). The `Param::eq` and `Param::is_close` methods were not following this convention and had `py` as a later argument in the signature. This commit corrects the oversight. * Fix merge conflict with #12943 With the recent merge with main we pulled in #12943 which conflicted with the rust space API changes made in this PR branch. This commit updates the usage to conform with the new interface introduced in this PR. * Add release notes and test for invalid args on apply methods This commit adds several release notes to document this change. This includes a feature note to describe the high level change and the user facing benefit (mainly reduced memory consumption for DAGCircuits), two upgrade notes to document the differences with shared references caused by the new data structure, and a fix note documenting the fix for how qargs and cargs are handled on `.apply_operation_back()` and `.apply_operation_front()`. Along with the fix note a new unit test is added to serve as a regression test so that we don't accidentally allow adding cargs as qargs and vice versa in the future. * Restore `inplace` argument functionality for substitute_node() This commit restores the functionality of the `inplace` argument for `substitute_node()` and restores the tests validating the object identity when using the flag. This flag was originally excluded from the implementation because the Rust representation of the dag is not a shared reference with Python space and the flag doesn't really mean the same thing as there is always a second copy of the data for Python space now. The implementation here is cheating slighty as we're passed in the DAG node by reference it relies on that reference to update the input node at the same time we update the dag. Unlike the previous Python implementation where we were updating the node in place and the `inplace` argument was slightly faster because everything was done by reference. The rust space data is still a compressed copy of the data we return to Python so the `inplace` flag will be slightly more inefficient as we need to copy to update the Python space representation in addition to the rust version. * Revert needless dict() cast on metadata in dag_to_circuit() This commit removes an unecessary `dict()` cast on the `dag.metadata` when setting it on `QuantumCircuit.metadata` in `qiskit.converters.dag_to_circuit()`. This slipped in at some point during the development of this PR and it's not clear why, but it isn't needed so this removes it. * Add code comment for DAGOpNode.__eq__ parameter checking This commit adds a small inline code comment to make it clear why we skip parameter comparisons in DAGOpNode.__eq__ for python ops. It might not be clear why the value is hard coded to `true` in this case, as this check is done via Python so we don't need to duplicate it in rust space. * Raise a ValueError on DAGNode creation with invalid index This commit adds error checking to the DAGNode constructor to raise a PyValueError if the input index is not valid (any index < -1). Previously this would have panicked instead of raising a user catchable error. * Use macro argument to set python getter/setter name This commit updates the function names for `get__node_id` and `set__node_id` method to use a name that clippy is happy with and leverage the pyo3 macros to set the python space name correctly instead of using the implicit naming rules. * Remove Ord and PartialOrd derives from interner::Index The Ord and PartialOrd traits were originally added to the Index struct so they could be used for the sort key in lexicographical topological sorting. However, that approach was abandonded during the development of this PR and instead the expanded Qubit and Clbit indices were used instead. This left the ordering traits as unnecessary on Index and potentially misleading. This commit just opts to remove them as they're not needed anymore. * Fix missing nodes in matplotlib drawer. Previously, the change in equality for DAGNodes was causing nodes to clobber eachother in the matplotlib drawer's tracking data structures when used as keys to maps. To fix this, we ensure that all nodes have a unique ID across layers before constructing the matplotlib drawer. They actually of course _do_ in the original DAG, but we don't really care what the original IDs are, so we just make them up. Writing to _node_id on a DAGNode may seem odd, but it exists in the old Python API (prior to being ported to Rust) and doesn't actually mutate the DAG at all since DAGNodes are ephemeral. * Revert "Update visual mpl circuit drawer references" With the previous commit the bug in the matplotlib drawer causing the images to diverge should be fixed. This commit reverts the change to the reference images as there should be no difference now. This reverts commit 1e4e6f386286b0b4e7f3ebd3f706f948dd707575. * Update visual mpl circuit drawer references for control flow circuits The earlier commit that "fixed" the drawers corrected the visualization to match expectations in most cases. However after restoring the references to what's on main several comparison tests with control flow in the circuit were still failing. The failure mode looks similar to the other cases, but across control flow blocks instead of at the circuit level. This commit temporarily updates the references of these to the state of what is generated currently to unblock CI. If/when we have a fix this commit can be reverted. * Fix edge cases in DAGOpNode.__eq__ This commit fixes a couple of edge cases in DAGOpNode.__eq__ method around the python interaction for the method. The first is that in the case where we had python object parameter types for the gates we weren't comparing them at all. This is fixed so we use python object equality for the params in this case. Then we were dropping the error handling in the case of using python for equality, this fixes it to return the error to users if the equality check fails. Finally a comment is added to explain the expected use case for `DAGOpNode.__eq__` and why parameter checking is more strict than elsewhere. * Remove Param::add() for global phase addition This commit removes the Param::add() method and instead adds a local private function to the `dag_circuit` module for doing global phase addition. Previously the `Param::add()` method was used solely for adding global phase in `DAGCircuit` and it took some shortcuts knowing that context. This made the method implementation ill suited as a general implementation. * More complete fix for matplotlib drawer. * Revert "Update visual mpl circuit drawer references for control flow circuits" This reverts commit 9a6f9536a3a7412d19a9fd9bbd761825c9a53d0f. * Unify rayon versions in workspace * Remove unused _GLOBAL_NID. * Use global monotonic ID counter for ids in drawer The fundamental issue with matplotlib visualizations of control flow is that locally in the control flow block the nodes look the same but are stored in an outer circuit dictionary. If the gates are the same and on the same qubits and happen to have the same node id inside the different control flow blocks the drawer would think it's already drawn the node and skip it incorrectly. The previous fix for this didn't go far enough because it wasn't accounting for the recursive execution of the drawer for inner blocks (it also didn't account for LayerSpoolers of the same length). * Re-add missing documentation * Remove unused BitData iterator stuff. * Make types, dag, and bit count methods public This commit makes some attributes of the dag circuit public as they will need to be accessible from the accelerate crate to realistically start using the DAGCircuit for rust transpiler passes. * Make Wire pickle serialization explicit This commit pivots away from using the PyO3 crate's conversion traits for specialized pickle serialization output of Wire objects. The output of the previous traits wasn't really intended for representing a Wire in Python but only for pickle serialization. This commit migrates these to custom methods, without a trait, to make it clear they're only for pickle. * Make py token usage explicit in _VarIndexMap The _VarIndexMap type was designed to look like an IndexMap but is actually an inner python dictionary. This is because `Var` types are still defined in python and we need to use a dictionary if we have `Var` objects as keys in the mapping. In the interest of looking like an IndexMap all the methods (except for 1) used `with_gil` internally to work with the dictionary. This could add unecessary overhead and to make it explicit that there is python involvement with this struct's methods this commit adds a py: Python argument to all the methods and removes the `with_gil` usage. * Make all pub(crate) visibility pub * Remove unused method * Reorganize code structure around PyVariableMapper and BitLocations * Add missing var wires to .get_wires() method In the porting of the get_wires() method to Rust the handling of Var wires was missed in the output of the method. This commit corrects the oversight and adds them to the output. * Raise TypeError not ValueError for invalid input to set_global_phase * De-duplicate check logic for op node adding methods The methods for checking the input was valid on apply_operation_back, apply_operation_front, and _apply_op_node_back were all identical. This combines them into a single method to deduplicate the code. * Improve collect_1q_runs() filter function The filter function for collect_1q_runs() was needlessly building a matrix for all the standard gates when all we need to know in that case is whether the standard gate is parameterized or not. If it's not then we're guaranteed to have a matrix available. This commit updates the filter logic to account for this and improve it's throughput on standard gates. * Use swap_remove instead of shift_remove * Combine input and output maps into single mapping This commit combines the `DAGCircuit` `qubit_input_map` and `qubit_output_map` fields into a single `IndexMap` `qubit_io_map` (and the same for `clbit_input_map` and `clbit_output_map` going to `clbit_io_map`). That stores the input and output as 2 element array where the first element is the input node index and the second element is the output node index. This reduces the number of lookups we need to do in practice and also reduces the memory overhead of `DAGCircuit`. * Ensure we account for clbits in depth() short circuit check * Also account for Vars in DAGCircuit.width() The number of vars should be included in the return from the width() method. This was previously missing in the new implementation of this method. * Remove duplicated _get_node() method The `_get_node()` method was duplicated with the already public `node()` method. This commit removes the duplicate and updates it's only usage in the code base. * Handle Var wires in classical_predecessors This method was missing the handling for var wires, this commit corrects the oversight. * Remove stray comment * Use Operation::control_flow() instead of isinstance checking * Use &str for increment_op and decrement_op This commit reworks the interface for the increment_op and decrement_op methods to work by reference instead of passing owned String objects to the methods. Using owned String objects was resulting in unecessary allocations and extra overhead that could be avoided. There are still a few places we needed to copy strings to ensure we're not mutating things while we have references to nodes in the dag, typically only in the decrement/removal case. But this commit reduces the number of String copies we need to make in the DAGCircuit. * Also include vars in depth short circuit * Fix typing for controlflow name lookup in count_ops * Fix .properties() method to include operations field The .properties() method should have included the output of .count_ops() in its dictionary return but this was commented out temporarily while other pieces of this PR were fixed. This commit circles back to it and adds the missing field from the output. As an aside we should probably deprecate the .properties() method for removal in 2.0 it doesn't seem to be the most useful method in practice. * Add missing Var wire handling to py_nodes_on_wire * Add back optimization to avoid isinstance in op_nodes This commit adds back an optimization to the op_nodes dag method to avoid doing a python space op comparison when we're filtering on non-standard gates and evaluating a standard gate. In these cases we know that the filter will not match purely from rust without needing a python space op object creation or an isinstance call so we can avoid the overhead of doing that. * Simplify/deduplicate __eq__ method This commit reworks the logic in the DAGCircuit.__eq__ method implementation to simplify the code a bit and make it less verbose and duplicated. * Invalidate cached py op when needed in substitute_node_with_dag This commit fixes a potential issue in substitute_node_with_dag() when the propagate_condition flag was set we were not invalidating cached py ops when adding a new condition based on a propagated condition. This could potentially cause the incorrect object to be returned to Python after calling this method. This fixes the issues by clearing the cached node so that when returning the op to python we are regenerating the python object. * Copy-editing suggestions for release notes Co-authored-by: John Lapeyre * Fix and simplify separable_circuits() This commit fixes and simplifies the separable_circuits() method. At it's core the method is building a subgraph of the original dag for each weakly connected component in the dag with a little bit of extra tracking to make sure the graph is a valid DAGCircuit. Instead of trying to do this manually this commit updates the method implementation to leverage the tools petgraph gives us for filtering graphs. This both fixes a bug identified in review but also simplifies the code. * Add clbit removal test * Move to using a Vec<[NodeIndex; 2]> for io maps This commit migrates the qubit_io_map and clbit_io_map to go from a type of `IndexMap` to `Vec<[NodeIndex; 2]>`. Our qubit indices (represented by the `Qubit` type) must be a contiguous set for the circuit to be valid, and using an `IndexMap` for these mappings of bit to input and output nodes only really improved performance in the removal case, but at the cost of additional runtime overhead for accessing the data. Since removals are rare and also inefficient because it needs to reindex the entire dag already we should instead optimize for the accessing the data. Since we have contiguous indices using a Vec is a natural structure to represent this mapping. * Make add_clbits() signature the same as add_qubits() At some point during the development of this PR the function signatures between `add_qubits()` and `add_clbits()` diverged between taking a `Vec>` and `&Bound`. In general they're are comprable but since we are going to be working with a `Vec<>` in the function body this is a better choice to let PyO3 worry about the conversion for us. Additionally, this is a more natural signature for rust consumption. This commit just updates `add_clbits()` to use a Vec too. * Add attribution comment to num_tensor_factors() method * Add py argument to add_declared_var() * Remove unnecessarily Python-space check * Correct typo in `to_pickle` method --------- Co-authored-by: Matthew Treinish Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Alexander Ivrii Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Co-authored-by: John Lapeyre Co-authored-by: Jake Lishman --- Cargo.lock | 6 + Cargo.toml | 6 +- crates/accelerate/Cargo.toml | 8 +- .../accelerate/src/convert_2q_block_matrix.rs | 26 +- .../src/euler_one_qubit_decomposer.rs | 24 +- crates/circuit/Cargo.toml | 14 +- crates/circuit/src/bit_data.rs | 45 +- crates/circuit/src/circuit_data.rs | 62 +- crates/circuit/src/circuit_instruction.rs | 19 +- crates/circuit/src/dag_circuit.rs | 6196 +++++++++++++++++ crates/circuit/src/dag_node.rs | 373 +- crates/circuit/src/dot_utils.rs | 109 + crates/circuit/src/error.rs | 16 + crates/circuit/src/imports.rs | 28 + crates/circuit/src/interner.rs | 83 +- crates/circuit/src/lib.rs | 29 +- crates/circuit/src/operations.rs | 25 + crates/circuit/src/packed_instruction.rs | 132 +- crates/circuit/src/rustworkx_core_vnext.rs | 1417 ++++ qiskit/converters/circuit_to_dag.py | 5 +- qiskit/dagcircuit/dagcircuit.py | 2403 +------ qiskit/dagcircuit/dagnode.py | 65 - .../two_qubit/two_qubit_decompose.py | 5 +- .../passes/basis/basis_translator.py | 6 +- .../passes/basis/unroll_3q_or_more.py | 4 +- .../passes/basis/unroll_custom_definitions.py | 4 +- .../passes/calibration/rzx_templates.py | 22 +- .../transpiler/passes/layout/apply_layout.py | 2 +- .../transpiler/passes/layout/sabre_layout.py | 2 +- .../optimization/commutative_cancellation.py | 4 +- .../passes/optimization/consolidate_blocks.py | 6 +- .../optimization/optimize_1q_decomposition.py | 6 +- .../passes/optimization/optimize_annotated.py | 6 +- .../passes/optimization/split_2q_unitaries.py | 7 +- .../transpiler/passes/routing/sabre_swap.py | 4 +- .../passes/routing/stochastic_swap.py | 6 +- .../padding/dynamical_decoupling.py | 5 +- .../scheduling/scheduling/base_scheduler.py | 4 +- .../passes/scheduling/time_unit_conversion.py | 2 +- .../passes/synthesis/high_level_synthesis.py | 6 +- .../passes/synthesis/unitary_synthesis.py | 12 +- .../transpiler/passes/utils/control_flow.py | 6 +- .../transpiler/passes/utils/gate_direction.py | 6 +- .../passes/utils/merge_adjacent_barriers.py | 2 +- qiskit/visualization/circuit/_utils.py | 33 +- qiskit/visualization/dag_visualization.py | 104 +- .../notes/dag-oxide-60b3d7219cb21703.yaml | 49 + test/python/compiler/test_transpiler.py | 22 +- test/python/dagcircuit/test_dagcircuit.py | 146 +- test/python/transpiler/_dummy_passes.py | 12 +- .../transpiler/test_collect_multiq_blocks.py | 1 + test/python/visualization/test_utils.py | 122 +- 52 files changed, 8726 insertions(+), 2981 deletions(-) create mode 100644 crates/circuit/src/dag_circuit.rs create mode 100644 crates/circuit/src/dot_utils.rs create mode 100644 crates/circuit/src/error.rs create mode 100644 crates/circuit/src/rustworkx_core_vnext.rs create mode 100644 releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml diff --git a/Cargo.lock b/Cargo.lock index 63e36f18645c..12daf2e6299e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1187,12 +1187,18 @@ dependencies = [ name = "qiskit-circuit" version = "1.3.0" dependencies = [ + "ahash 0.8.11", + "approx", "bytemuck", "hashbrown 0.14.5", + "indexmap", + "itertools 0.13.0", "ndarray", "num-complex", "numpy", "pyo3", + "rayon", + "rustworkx-core", "smallvec", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index f6141f787fdf..a35a87d4d3b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,18 @@ license = "Apache-2.0" [workspace.dependencies] bytemuck = "1.17" indexmap.version = "2.4.0" -hashbrown.version = "0.14.0" +hashbrown.version = "0.14.5" num-bigint = "0.4" num-complex = "0.4" ndarray = "^0.15.6" numpy = "0.21.0" smallvec = "1.13" thiserror = "1.0" +rustworkx-core = "0.15" +approx = "0.5" +itertools = "0.13.0" ahash = "0.8.11" +rayon = "1.10" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index c93e81b14dee..838fc0153577 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -10,7 +10,7 @@ name = "qiskit_accelerate" doctest = false [dependencies] -rayon = "1.10" +rayon.workspace = true numpy.workspace = true rand = "0.8" rand_pcg = "0.3" @@ -18,10 +18,10 @@ rand_distr = "0.4.3" ahash.workspace = true num-traits = "0.2" num-complex.workspace = true +rustworkx-core.workspace = true num-bigint.workspace = true -rustworkx-core = "0.15" faer = "0.19.1" -itertools = "0.13.0" +itertools.workspace = true qiskit-circuit.workspace = true thiserror.workspace = true @@ -38,7 +38,7 @@ workspace = true features = ["rayon", "approx-0_5"] [dependencies.approx] -version = "0.5" +workspace = true features = ["num-complex"] [dependencies.hashbrown] diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index e9f6e343b6bd..dc4d0b77c4a7 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -27,7 +27,7 @@ use qiskit_circuit::circuit_instruction::CircuitInstruction; use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; use qiskit_circuit::imports::QI_OPERATOR; -use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::operations::Operation; use crate::QiskitError; @@ -35,7 +35,7 @@ fn get_matrix_from_inst<'py>( py: Python<'py>, inst: &'py CircuitInstruction, ) -> PyResult> { - if let Some(mat) = inst.op().matrix(&inst.params) { + if let Some(mat) = inst.operation.matrix(&inst.params) { Ok(mat) } else if inst.operation.try_standard_gate().is_some() { Err(QiskitError::new_err( @@ -124,29 +124,7 @@ pub fn change_basis(matrix: ArrayView2) -> Array2 { trans_matrix } -#[pyfunction] -pub fn collect_2q_blocks_filter(node: &Bound) -> Option { - let Ok(node) = node.downcast::() else { - return None; - }; - let node = node.borrow(); - match node.instruction.op() { - gate @ (OperationRef::Standard(_) | OperationRef::Gate(_)) => Some( - gate.num_qubits() <= 2 - && node - .instruction - .extra_attrs - .as_ref() - .and_then(|attrs| attrs.condition.as_ref()) - .is_none() - && !node.is_parameterized(), - ), - _ => Some(false), - } -} - pub fn convert_2q_block_matrix(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(blocks_to_matrix))?; - m.add_wrapped(wrap_pyfunction!(collect_2q_blocks_filter))?; Ok(()) } diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 7463777af624..75a2f7993f86 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -743,7 +743,7 @@ pub fn compute_error_list( .iter() .map(|node| { ( - node.instruction.op().name().to_string(), + node.instruction.operation.name().to_string(), smallvec![], // Params not needed in this path ) }) @@ -988,10 +988,11 @@ pub fn optimize_1q_gates_decomposition( .iter() .map(|node| { if let Some(err_map) = error_map { - error *= compute_error_term(node.instruction.op().name(), err_map, qubit) + error *= + compute_error_term(node.instruction.operation.name(), err_map, qubit) } node.instruction - .op() + .operation .matrix(&node.instruction.params) .expect("No matrix defined for operation") }) @@ -1043,22 +1044,6 @@ fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { ]; } -#[pyfunction] -pub fn collect_1q_runs_filter(node: &Bound) -> bool { - let Ok(node) = node.downcast::() else { - return false; - }; - let node = node.borrow(); - let op = node.instruction.op(); - op.num_qubits() == 1 - && op.num_clbits() == 0 - && op.matrix(&node.instruction.params).is_some() - && match &node.instruction.extra_attrs { - None => true, - Some(attrs) => attrs.condition.is_none(), - } -} - pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_zyz))?; m.add_wrapped(wrap_pyfunction!(params_xyx))?; @@ -1072,7 +1057,6 @@ pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(compute_error_one_qubit_sequence))?; m.add_wrapped(wrap_pyfunction!(compute_error_list))?; m.add_wrapped(wrap_pyfunction!(optimize_1q_gates_decomposition))?; - m.add_wrapped(wrap_pyfunction!(collect_1q_runs_filter))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 3eb430515fcf..ed1f849bbf62 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -10,17 +10,29 @@ name = "qiskit_circuit" doctest = false [dependencies] +rayon.workspace = true +ahash.workspace = true +rustworkx-core.workspace = true bytemuck.workspace = true -hashbrown.workspace = true num-complex.workspace = true ndarray.workspace = true numpy.workspace = true thiserror.workspace = true +approx.workspace = true +itertools.workspace = true [dependencies.pyo3] workspace = true features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] +[dependencies.hashbrown] +workspace = true +features = ["rayon"] + +[dependencies.indexmap] +workspace = true +features = ["rayon"] + [dependencies.smallvec] workspace = true features = ["union"] diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 977d1b34e496..0c0b20a02522 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -81,17 +81,6 @@ pub struct BitData { cached: Py, } -pub struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>); - -impl<'py> From> for PyErr { - fn from(error: BitNotFoundError) -> Self { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - error.0 - )) - } -} - impl BitData where T: From + Copy, @@ -139,14 +128,19 @@ where pub fn map_bits<'py>( &self, bits: impl IntoIterator>, - ) -> Result, BitNotFoundError<'py>> { + ) -> PyResult> { let v: Result, _> = bits .into_iter() .map(|b| { self.indices .get(&BitAsKey::new(&b)) .copied() - .ok_or_else(|| BitNotFoundError(b)) + .ok_or_else(|| { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + b + )) + }) }) .collect(); v.map(|x| x.into_iter()) @@ -168,7 +162,7 @@ where } /// Adds a new Python bit. - pub fn add(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { + pub fn add(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult { if self.bits.len() != self.cached.bind(bit.py()).len() { return Err(PyRuntimeError::new_err( format!("This circuit's {} list has become out of sync with the circuit data. Did something modify it?", self.description) @@ -193,6 +187,29 @@ where bit ))); } + Ok(idx.into()) + } + + pub fn remove_indices(&mut self, py: Python, indices: I) -> PyResult<()> + where + I: IntoIterator, + { + let mut indices_sorted: Vec = indices + .into_iter() + .map(|i| >::from(i) as usize) + .collect(); + indices_sorted.sort(); + + for index in indices_sorted.into_iter().rev() { + self.cached.bind(py).del_item(index)?; + let bit = self.bits.remove(index); + self.indices.remove(&BitAsKey::new(bit.bind(py))); + } + // Update indices. + for (i, bit) in self.bits.iter().enumerate() { + self.indices + .insert(BitAsKey::new(bit.bind(py)), (i as BitType).into()); + } Ok(()) } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index d29455c5363b..4dd3956bee6a 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -16,7 +16,7 @@ use std::cell::OnceCell; use crate::bit_data::BitData; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; -use crate::interner::{IndexedInterner, Interner, InternerKey}; +use crate::interner::{IndexedInterner, Interner}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; use crate::packed_instruction::{PackedInstruction, PackedOperation}; use crate::parameter_table::{ParameterTable, ParameterTableError, ParameterUse, ParameterUuid}; @@ -148,12 +148,8 @@ impl CircuitData { global_phase, )?; for (operation, params, qargs, cargs) in instruction_iter { - let qubits = (&mut res.qargs_interner) - .intern(InternerKey::Value(qargs))? - .index; - let clbits = (&mut res.cargs_interner) - .intern(InternerKey::Value(cargs))? - .index; + let qubits = (&mut res.qargs_interner).intern(qargs)?; + let clbits = (&mut res.cargs_interner).intern(cargs)?; let params = (!params.is_empty()).then(|| Box::new(params)); res.data.push(PackedInstruction { op: operation, @@ -203,13 +199,9 @@ impl CircuitData { instruction_iter.size_hint().0, global_phase, )?; - let no_clbit_index = (&mut res.cargs_interner) - .intern(InternerKey::Value(Vec::new()))? - .index; + let no_clbit_index = (&mut res.cargs_interner).intern(Vec::new())?; for (operation, params, qargs) in instruction_iter { - let qubits = (&mut res.qargs_interner) - .intern(InternerKey::Value(qargs.to_vec()))? - .index; + let qubits = (&mut res.qargs_interner).intern(qargs.to_vec())?; let params = (!params.is_empty()).then(|| Box::new(params)); res.data.push(PackedInstruction { op: operation.into(), @@ -266,13 +258,9 @@ impl CircuitData { params: &[Param], qargs: &[Qubit], ) -> PyResult<()> { - let no_clbit_index = (&mut self.cargs_interner) - .intern(InternerKey::Value(Vec::new()))? - .index; + let no_clbit_index = (&mut self.cargs_interner).intern(Vec::new())?; let params = (!params.is_empty()).then(|| Box::new(params.iter().cloned().collect())); - let qubits = (&mut self.qargs_interner) - .intern(InternerKey::Value(qargs.to_vec()))? - .index; + let qubits = (&mut self.qargs_interner).intern(qargs.to_vec())?; self.data.push(PackedInstruction { op: operation.into(), qubits, @@ -497,7 +485,8 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_qubit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - self.qubits.add(py, bit, strict) + self.qubits.add(py, bit, strict)?; + Ok(()) } /// Registers a :class:`.Clbit` instance. @@ -511,7 +500,8 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_clbit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - self.clbits.add(py, bit, strict) + self.clbits.add(py, bit, strict)?; + Ok(()) } /// Performs a shallow copy. @@ -582,10 +572,10 @@ impl CircuitData { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(py)?; for inst in self.data.iter() { - for b in self.qargs_interner.intern(inst.qubits).value.iter() { + for b in self.qargs_interner.intern(inst.qubits) { qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; } - for b in self.cargs_interner.intern(inst.clbits).value.iter() { + for b in self.cargs_interner.intern(inst.clbits) { clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?; } } @@ -751,8 +741,8 @@ impl CircuitData { let clbits = self.cargs_interner.intern(inst.clbits); CircuitInstruction { operation: inst.op.clone(), - qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits.value)).unbind(), - clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits.value)).unbind(), + qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits)).unbind(), + clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits)).unbind(), params: inst.params_view().iter().cloned().collect(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -905,7 +895,6 @@ impl CircuitData { let qubits = other .qargs_interner .intern(inst.qubits) - .value .iter() .map(|b| { Ok(self @@ -917,7 +906,6 @@ impl CircuitData { let clbits = other .cargs_interner .intern(inst.clbits) - .value .iter() .map(|b| { Ok(self @@ -927,14 +915,12 @@ impl CircuitData { }) .collect::>>()?; let new_index = self.data.len(); - let qubits_id = - Interner::intern(&mut self.qargs_interner, InternerKey::Value(qubits))?; - let clbits_id = - Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; + let qubits_id = Interner::intern(&mut self.qargs_interner, qubits)?; + let clbits_id = Interner::intern(&mut self.cargs_interner, clbits)?; self.data.push(PackedInstruction { op: inst.op.clone(), - qubits: qubits_id.index, - clbits: clbits_id.index, + qubits: qubits_id, + clbits: clbits_id, params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -1106,7 +1092,7 @@ impl CircuitData { pub fn num_nonlocal_gates(&self) -> usize { self.data .iter() - .filter(|inst| inst.op().num_qubits() > 1 && !inst.op().directive()) + .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) .count() } } @@ -1129,16 +1115,16 @@ impl CircuitData { fn pack(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { let qubits = Interner::intern( &mut self.qargs_interner, - InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), + self.qubits.map_bits(inst.qubits.bind(py))?.collect(), )?; let clbits = Interner::intern( &mut self.cargs_interner, - InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), + self.clbits.map_bits(inst.clbits.bind(py))?.collect(), )?; Ok(PackedInstruction { op: inst.operation.clone(), - qubits: qubits.index, - clbits: clbits.index, + qubits, + clbits, params: (!inst.params.is_empty()).then(|| Box::new(inst.params.clone())), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ae649b5a8110..b0620d78fb75 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -114,11 +114,6 @@ pub struct CircuitInstruction { } impl CircuitInstruction { - /// View the operation in this `CircuitInstruction`. - pub fn op(&self) -> OperationRef { - self.operation.view() - } - /// Get the Python-space operation, ensuring that it is mutable from Python space (singleton /// gates might not necessarily satisfy this otherwise). /// @@ -135,6 +130,12 @@ impl CircuitInstruction { out.call_method0(intern!(py, "to_mutable")) } } + + pub fn condition(&self) -> Option<&PyObject> { + self.extra_attrs + .as_ref() + .and_then(|args| args.condition.as_ref()) + } } #[pymethods] @@ -230,7 +231,7 @@ impl CircuitInstruction { /// Returns the Instruction name corresponding to the op for this node #[getter] fn get_name(&self, py: Python) -> PyObject { - self.op().name().to_object(py) + self.operation.name().to_object(py) } #[getter] @@ -252,7 +253,7 @@ impl CircuitInstruction { } #[getter] - fn condition(&self, py: Python) -> Option { + fn get_condition(&self, py: Python) -> Option { self.extra_attrs .as_ref() .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) @@ -292,13 +293,13 @@ impl CircuitInstruction { /// Is the :class:`.Operation` contained in this node a directive? pub fn is_directive(&self) -> bool { - self.op().directive() + self.operation.directive() } /// Is the :class:`.Operation` contained in this instruction a control-flow operation (i.e. an /// instance of :class:`.ControlFlowOp`)? pub fn is_control_flow(&self) -> bool { - self.op().control_flow() + self.operation.control_flow() } /// Does this instruction contain any :class:`.ParameterExpression` parameters? diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs new file mode 100644 index 000000000000..bb5fff5e343a --- /dev/null +++ b/crates/circuit/src/dag_circuit.rs @@ -0,0 +1,6196 @@ +// 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 std::hash::{Hash, Hasher}; + +use ahash::RandomState; + +use crate::bit_data::BitData; +use crate::circuit_instruction::{ + CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, +}; +use crate::dag_node::{DAGInNode, DAGNode, DAGOpNode, DAGOutNode}; +use crate::dot_utils::build_dot; +use crate::error::DAGCircuitError; +use crate::imports; +use crate::interner::{IndexedInterner, Interner}; +use crate::operations::{Operation, OperationRef, Param, PyInstruction}; +use crate::packed_instruction::PackedInstruction; +use crate::rustworkx_core_vnext::isomorphism; +use crate::{BitType, Clbit, Qubit, TupleLikeArg}; + +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexMap; +use itertools::Itertools; + +use pyo3::exceptions::{PyIndexError, PyRuntimeError, PyTypeError, PyValueError}; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::{ + IntoPyDict, PyDict, PyInt, PyIterator, PyList, PySequence, PySet, PyString, PyTuple, PyType, +}; + +use rustworkx_core::dag_algo::layers; +use rustworkx_core::err::ContractError; +use rustworkx_core::graph_ext::ContractNodesDirected; +use rustworkx_core::petgraph; +use rustworkx_core::petgraph::prelude::StableDiGraph; +use rustworkx_core::petgraph::prelude::*; +use rustworkx_core::petgraph::stable_graph::{EdgeReference, NodeIndex}; +use rustworkx_core::petgraph::unionfind::UnionFind; +use rustworkx_core::petgraph::visit::{ + EdgeIndexable, IntoEdgeReferences, IntoNodeReferences, NodeFiltered, NodeIndexable, +}; +use rustworkx_core::petgraph::Incoming; +use rustworkx_core::traversal::{ + ancestors as core_ancestors, bfs_successors as core_bfs_successors, + descendants as core_descendants, +}; + +use std::cmp::Ordering; +use std::collections::{BTreeMap, VecDeque}; +use std::convert::Infallible; +use std::f64::consts::PI; + +#[cfg(feature = "cache_pygates")] +use std::cell::OnceCell; + +static CONTROL_FLOW_OP_NAMES: [&str; 4] = ["for_loop", "while_loop", "if_else", "switch_case"]; +static SEMANTIC_EQ_SYMMETRIC: [&str; 4] = ["barrier", "swap", "break_loop", "continue_loop"]; + +#[derive(Clone, Debug)] +pub enum NodeType { + QubitIn(Qubit), + QubitOut(Qubit), + ClbitIn(Clbit), + ClbitOut(Clbit), + VarIn(PyObject), + VarOut(PyObject), + Operation(PackedInstruction), +} + +#[derive(Clone, Debug)] +pub enum Wire { + Qubit(Qubit), + Clbit(Clbit), + Var(PyObject), +} + +impl PartialEq for Wire { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Wire::Qubit(q1), Wire::Qubit(q2)) => q1 == q2, + (Wire::Clbit(c1), Wire::Clbit(c2)) => c1 == c2, + (Wire::Var(v1), Wire::Var(v2)) => { + v1.is(v2) || Python::with_gil(|py| v1.bind(py).eq(v2).unwrap()) + } + _ => false, + } + } +} + +impl Eq for Wire {} + +impl Hash for Wire { + fn hash(&self, state: &mut H) { + match self { + Self::Qubit(qubit) => qubit.hash(state), + Self::Clbit(clbit) => clbit.hash(state), + Self::Var(var) => Python::with_gil(|py| var.bind(py).hash().unwrap().hash(state)), + } + } +} + +impl Wire { + fn to_pickle(&self, py: Python) -> PyObject { + match self { + Self::Qubit(bit) => (0, bit.0.into_py(py)).into_py(py), + Self::Clbit(bit) => (1, bit.0.into_py(py)).into_py(py), + Self::Var(var) => (2, var.clone_ref(py)).into_py(py), + } + } + + fn from_pickle(b: &Bound) -> PyResult { + let tuple: Bound = b.extract()?; + let wire_type: usize = tuple.get_item(0)?.extract()?; + if wire_type == 0 { + Ok(Self::Qubit(Qubit(tuple.get_item(1)?.extract()?))) + } else if wire_type == 1 { + Ok(Self::Clbit(Clbit(tuple.get_item(1)?.extract()?))) + } else if wire_type == 2 { + Ok(Self::Var(tuple.get_item(1)?.unbind())) + } else { + Err(PyTypeError::new_err("Invalid wire type")) + } + } +} + +// TODO: Remove me. +// This is a temporary map type used to store a mapping of +// Var to NodeIndex to hold us over until Var is ported to +// Rust. Currently, we need this because PyObject cannot be +// used as the key to an IndexMap. +// +// Once we've got Var ported, Wire should also become Hash + Eq +// and we can consider combining input/output nodes maps. +#[derive(Clone, Debug)] +struct _VarIndexMap { + dict: Py, +} + +impl _VarIndexMap { + pub fn new(py: Python) -> Self { + Self { + dict: PyDict::new_bound(py).unbind(), + } + } + + pub fn keys(&self, py: Python) -> impl Iterator { + self.dict + .bind(py) + .keys() + .into_iter() + .map(|k| k.unbind()) + .collect::>() + .into_iter() + } + + pub fn contains_key(&self, py: Python, key: &PyObject) -> bool { + self.dict.bind(py).contains(key).unwrap() + } + + pub fn get(&self, py: Python, key: &PyObject) -> Option { + self.dict + .bind(py) + .get_item(key) + .unwrap() + .map(|v| NodeIndex::new(v.extract().unwrap())) + } + + pub fn insert(&mut self, py: Python, key: PyObject, value: NodeIndex) { + self.dict + .bind(py) + .set_item(key, value.index().into_py(py)) + .unwrap() + } + + pub fn remove(&mut self, py: Python, key: &PyObject) -> Option { + let bound_dict = self.dict.bind(py); + let res = bound_dict + .get_item(key.clone_ref(py)) + .unwrap() + .map(|v| NodeIndex::new(v.extract().unwrap())); + let _del_result = bound_dict.del_item(key); + res + } + pub fn values<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { + let values = self.dict.bind(py).values(); + values.iter().map(|x| NodeIndex::new(x.extract().unwrap())) + } + + pub fn iter<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { + self.dict + .bind(py) + .iter() + .map(|(var, index)| (var.unbind(), NodeIndex::new(index.extract().unwrap()))) + } +} + +/// Quantum circuit as a directed acyclic graph. +/// +/// There are 3 types of nodes in the graph: inputs, outputs, and operations. +/// The nodes are connected by directed edges that correspond to qubits and +/// bits. +#[pyclass(module = "qiskit._accelerate.circuit")] +#[derive(Clone, Debug)] +pub struct DAGCircuit { + /// Circuit name. Generally, this corresponds to the name + /// of the QuantumCircuit from which the DAG was generated. + #[pyo3(get, set)] + name: Option, + /// Circuit metadata + #[pyo3(get, set)] + metadata: Option, + + calibrations: HashMap>, + + pub dag: StableDiGraph, + + #[pyo3(get)] + qregs: Py, + #[pyo3(get)] + cregs: Py, + + /// The cache used to intern instruction qargs. + qargs_cache: IndexedInterner>, + /// The cache used to intern instruction cargs. + cargs_cache: IndexedInterner>, + /// Qubits registered in the circuit. + pub qubits: BitData, + /// Clbits registered in the circuit. + pub clbits: BitData, + /// Global phase. + global_phase: Param, + /// Duration. + #[pyo3(get, set)] + duration: Option, + /// Unit of duration. + #[pyo3(get, set)] + unit: String, + + // Note: these are tracked separately from `qubits` and `clbits` + // because it's not yet clear if the Rust concept of a native Qubit + // and Clbit should correspond directly to the numerical Python + // index that users see in the Python API. + /// The index locations of bits, and their positions within + /// registers. + qubit_locations: Py, + clbit_locations: Py, + + /// Map from qubit to input and output nodes of the graph. + qubit_io_map: Vec<[NodeIndex; 2]>, + + /// Map from clbit to input and output nodes of the graph. + clbit_io_map: Vec<[NodeIndex; 2]>, + + // TODO: use IndexMap once Var is ported to Rust + /// Map from var to input nodes of the graph. + var_input_map: _VarIndexMap, + /// Map from var to output nodes of the graph. + var_output_map: _VarIndexMap, + + /// Operation kind to count + op_names: IndexMap, + + // Python modules we need to frequently access (for now). + control_flow_module: PyControlFlowModule, + vars_info: HashMap, + vars_by_type: [Py; 3], +} + +#[derive(Clone, Debug)] +struct PyControlFlowModule { + condition_resources: Py, + node_resources: Py, +} + +#[derive(Clone, Debug)] +struct PyLegacyResources { + clbits: Py, + cregs: Py, +} + +impl PyControlFlowModule { + fn new(py: Python) -> PyResult { + let module = PyModule::import_bound(py, "qiskit.circuit.controlflow")?; + Ok(PyControlFlowModule { + condition_resources: module.getattr("condition_resources")?.unbind(), + node_resources: module.getattr("node_resources")?.unbind(), + }) + } + + fn condition_resources(&self, condition: &Bound) -> PyResult { + let res = self + .condition_resources + .bind(condition.py()) + .call1((condition,))?; + Ok(PyLegacyResources { + clbits: res.getattr("clbits")?.downcast_into_exact()?.unbind(), + cregs: res.getattr("cregs")?.downcast_into_exact()?.unbind(), + }) + } + + fn node_resources(&self, node: &Bound) -> PyResult { + let res = self.node_resources.bind(node.py()).call1((node,))?; + Ok(PyLegacyResources { + clbits: res.getattr("clbits")?.downcast_into_exact()?.unbind(), + cregs: res.getattr("cregs")?.downcast_into_exact()?.unbind(), + }) + } +} + +struct PyVariableMapper { + mapper: Py, +} + +impl PyVariableMapper { + fn new( + py: Python, + target_cregs: Bound, + bit_map: Option>, + var_map: Option>, + add_register: Option>, + ) -> PyResult { + let kwargs: HashMap<&str, Option>> = + HashMap::from_iter([("add_register", add_register)]); + Ok(PyVariableMapper { + mapper: imports::VARIABLE_MAPPER + .get_bound(py) + .call( + (target_cregs, bit_map, var_map), + Some(&kwargs.into_py_dict_bound(py)), + )? + .unbind(), + }) + } + + fn map_condition<'py>( + &self, + condition: &Bound<'py, PyAny>, + allow_reorder: bool, + ) -> PyResult> { + let py = condition.py(); + let kwargs: HashMap<&str, Py> = + HashMap::from_iter([("allow_reorder", allow_reorder.into_py(py))]); + self.mapper.bind(py).call_method( + intern!(py, "map_condition"), + (condition,), + Some(&kwargs.into_py_dict_bound(py)), + ) + } + + fn map_target<'py>(&self, target: &Bound<'py, PyAny>) -> PyResult> { + let py = target.py(); + self.mapper + .bind(py) + .call_method1(intern!(py, "map_target"), (target,)) + } +} + +impl IntoPy> for PyVariableMapper { + fn into_py(self, _py: Python<'_>) -> Py { + self.mapper + } +} + +#[pyfunction] +fn reject_new_register(reg: &Bound) -> PyResult<()> { + Err(DAGCircuitError::new_err(format!( + "No register with '{:?}' to map this expression onto.", + reg.getattr("bits")? + ))) +} + +#[pyclass(module = "qiskit._accelerate.circuit")] +#[derive(Clone, Debug)] +struct BitLocations { + #[pyo3(get)] + index: usize, + #[pyo3(get)] + registers: Py, +} + +#[derive(Copy, Clone, Debug)] +enum DAGVarType { + Input = 0, + Capture = 1, + Declare = 2, +} + +#[derive(Clone, Debug)] +struct DAGVarInfo { + var: PyObject, + type_: DAGVarType, + in_node: NodeIndex, + out_node: NodeIndex, +} + +#[pymethods] +impl DAGCircuit { + #[new] + pub fn new(py: Python<'_>) -> PyResult { + Ok(DAGCircuit { + name: None, + metadata: Some(PyDict::new_bound(py).unbind().into()), + calibrations: HashMap::new(), + dag: StableDiGraph::default(), + qregs: PyDict::new_bound(py).unbind(), + cregs: PyDict::new_bound(py).unbind(), + qargs_cache: IndexedInterner::new(), + cargs_cache: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + global_phase: Param::Float(0.), + duration: None, + unit: "dt".to_string(), + qubit_locations: PyDict::new_bound(py).unbind(), + clbit_locations: PyDict::new_bound(py).unbind(), + qubit_io_map: Vec::new(), + clbit_io_map: Vec::new(), + var_input_map: _VarIndexMap::new(py), + var_output_map: _VarIndexMap::new(py), + op_names: IndexMap::default(), + control_flow_module: PyControlFlowModule::new(py)?, + vars_info: HashMap::new(), + vars_by_type: [ + PySet::empty_bound(py)?.unbind(), + PySet::empty_bound(py)?.unbind(), + PySet::empty_bound(py)?.unbind(), + ], + }) + } + + #[getter] + fn input_map(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + for (qubit, indices) in self + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + out_dict.set_item( + self.qubits.get(qubit).unwrap().clone_ref(py), + self.get_node(py, indices[0])?, + )?; + } + for (clbit, indices) in self + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + out_dict.set_item( + self.clbits.get(clbit).unwrap().clone_ref(py), + self.get_node(py, indices[0])?, + )?; + } + for (var, index) in self.var_input_map.dict.bind(py).iter() { + out_dict.set_item( + var, + self.get_node(py, NodeIndex::new(index.extract::()?))?, + )?; + } + Ok(out_dict.unbind()) + } + + #[getter] + fn output_map(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + for (qubit, indices) in self + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + out_dict.set_item( + self.qubits.get(qubit).unwrap().clone_ref(py), + self.get_node(py, indices[1])?, + )?; + } + for (clbit, indices) in self + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + out_dict.set_item( + self.clbits.get(clbit).unwrap().clone_ref(py), + self.get_node(py, indices[1])?, + )?; + } + for (var, index) in self.var_output_map.dict.bind(py).iter() { + out_dict.set_item( + var, + self.get_node(py, NodeIndex::new(index.extract::()?))?, + )?; + } + Ok(out_dict.unbind()) + } + + fn __getstate__(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + out_dict.set_item("name", self.name.as_ref().map(|x| x.clone_ref(py)))?; + out_dict.set_item("metadata", self.metadata.as_ref().map(|x| x.clone_ref(py)))?; + out_dict.set_item("calibrations", self.calibrations.clone())?; + out_dict.set_item("qregs", self.qregs.clone_ref(py))?; + out_dict.set_item("cregs", self.cregs.clone_ref(py))?; + out_dict.set_item("global_phase", self.global_phase.clone())?; + out_dict.set_item( + "qubit_io_map", + self.qubit_io_map + .iter() + .enumerate() + .map(|(k, v)| (k, [v[0].index(), v[1].index()])) + .collect::>(), + )?; + out_dict.set_item( + "clbit_io_map", + self.clbit_io_map + .iter() + .enumerate() + .map(|(k, v)| (k, [v[0].index(), v[1].index()])) + .collect::>(), + )?; + out_dict.set_item("var_input_map", self.var_input_map.dict.clone_ref(py))?; + out_dict.set_item("var_output_map", self.var_output_map.dict.clone_ref(py))?; + out_dict.set_item("op_name", self.op_names.clone())?; + out_dict.set_item( + "vars_info", + self.vars_info + .iter() + .map(|(k, v)| { + ( + k, + ( + v.var.clone_ref(py), + v.type_ as u8, + v.in_node.index(), + v.out_node.index(), + ), + ) + }) + .collect::>(), + )?; + out_dict.set_item("vars_by_type", self.vars_by_type.clone())?; + out_dict.set_item("qubits", self.qubits.bits())?; + out_dict.set_item("clbits", self.clbits.bits())?; + let mut nodes: Vec = Vec::with_capacity(self.dag.node_count()); + for node_idx in self.dag.node_indices() { + let node_data = self.get_node(py, node_idx)?; + nodes.push((node_idx.index(), node_data).to_object(py)); + } + out_dict.set_item("nodes", nodes)?; + out_dict.set_item( + "nodes_removed", + self.dag.node_count() != self.dag.node_bound(), + )?; + let mut edges: Vec = Vec::with_capacity(self.dag.edge_bound()); + // edges are saved with none (deleted edges) instead of their index to save space + for i in 0..self.dag.edge_bound() { + let idx = EdgeIndex::new(i); + let edge = match self.dag.edge_weight(idx) { + Some(edge_w) => { + let endpoints = self.dag.edge_endpoints(idx).unwrap(); + ( + endpoints.0.index(), + endpoints.1.index(), + edge_w.clone().to_pickle(py), + ) + .to_object(py) + } + None => py.None(), + }; + edges.push(edge); + } + out_dict.set_item("edges", edges)?; + Ok(out_dict.unbind()) + } + + fn __setstate__(&mut self, py: Python, state: PyObject) -> PyResult<()> { + let dict_state = state.downcast_bound::(py)?; + self.name = dict_state.get_item("name")?.unwrap().extract()?; + self.metadata = dict_state.get_item("metadata")?.unwrap().extract()?; + self.calibrations = dict_state.get_item("calibrations")?.unwrap().extract()?; + self.qregs = dict_state.get_item("qregs")?.unwrap().extract()?; + self.cregs = dict_state.get_item("cregs")?.unwrap().extract()?; + self.global_phase = dict_state.get_item("global_phase")?.unwrap().extract()?; + self.op_names = dict_state.get_item("op_name")?.unwrap().extract()?; + self.var_input_map = _VarIndexMap { + dict: dict_state.get_item("var_input_map")?.unwrap().extract()?, + }; + self.var_output_map = _VarIndexMap { + dict: dict_state.get_item("var_output_map")?.unwrap().extract()?, + }; + self.vars_by_type = dict_state.get_item("vars_by_type")?.unwrap().extract()?; + let binding = dict_state.get_item("vars_info")?.unwrap(); + let vars_info_raw = binding.downcast::().unwrap(); + self.vars_info = HashMap::with_capacity(vars_info_raw.len()); + for (key, value) in vars_info_raw.iter() { + let val_tuple = value.downcast::()?; + let info = DAGVarInfo { + var: val_tuple.get_item(0)?.unbind(), + type_: match val_tuple.get_item(1)?.extract::()? { + 0 => DAGVarType::Input, + 1 => DAGVarType::Capture, + 2 => DAGVarType::Declare, + _ => return Err(PyValueError::new_err("Invalid var type")), + }, + in_node: NodeIndex::new(val_tuple.get_item(2)?.extract()?), + out_node: NodeIndex::new(val_tuple.get_item(3)?.extract()?), + }; + self.vars_info.insert(key.extract()?, info); + } + + let binding = dict_state.get_item("qubits")?.unwrap(); + let qubits_raw = binding.downcast::().unwrap(); + for bit in qubits_raw.iter() { + self.qubits.add(py, &bit, false)?; + } + let binding = dict_state.get_item("clbits")?.unwrap(); + let clbits_raw = binding.downcast::().unwrap(); + for bit in clbits_raw.iter() { + self.clbits.add(py, &bit, false)?; + } + let binding = dict_state.get_item("qubit_io_map")?.unwrap(); + let qubit_index_map_raw = binding.downcast::().unwrap(); + self.qubit_io_map = Vec::with_capacity(qubit_index_map_raw.len()); + for (_k, v) in qubit_index_map_raw.iter() { + let indices: [usize; 2] = v.extract()?; + self.qubit_io_map + .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); + } + let binding = dict_state.get_item("clbit_io_map")?.unwrap(); + let clbit_index_map_raw = binding.downcast::().unwrap(); + self.clbit_io_map = Vec::with_capacity(clbit_index_map_raw.len()); + + for (_k, v) in clbit_index_map_raw.iter() { + let indices: [usize; 2] = v.extract()?; + self.clbit_io_map + .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); + } + // Rebuild Graph preserving index holes: + let binding = dict_state.get_item("nodes")?.unwrap(); + let nodes_lst = binding.downcast::()?; + let binding = dict_state.get_item("edges")?.unwrap(); + let edges_lst = binding.downcast::()?; + let node_removed: bool = dict_state.get_item("nodes_removed")?.unwrap().extract()?; + self.dag = StableDiGraph::default(); + if !node_removed { + for item in nodes_lst.iter() { + let node_w = item.downcast::().unwrap().get_item(1).unwrap(); + let weight = self.pack_into(py, &node_w)?; + self.dag.add_node(weight); + } + } else if nodes_lst.len() == 1 { + // graph has only one node, handle logic here to save one if in the loop later + let binding = nodes_lst.get_item(0).unwrap(); + let item = binding.downcast::().unwrap(); + let node_idx: usize = item.get_item(0).unwrap().extract().unwrap(); + let node_w = item.get_item(1).unwrap(); + + for _i in 0..node_idx { + self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + } + let weight = self.pack_into(py, &node_w)?; + self.dag.add_node(weight); + for i in 0..node_idx { + self.dag.remove_node(NodeIndex::new(i)); + } + } else { + let binding = nodes_lst.get_item(nodes_lst.len() - 1).unwrap(); + let last_item = binding.downcast::().unwrap(); + + // list of temporary nodes that will be removed later to re-create holes + let node_bound_1: usize = last_item.get_item(0).unwrap().extract().unwrap(); + let mut tmp_nodes: Vec = + Vec::with_capacity(node_bound_1 + 1 - nodes_lst.len()); + + for item in nodes_lst { + let item = item.downcast::().unwrap(); + let next_index: usize = item.get_item(0).unwrap().extract().unwrap(); + let weight: PyObject = item.get_item(1).unwrap().extract().unwrap(); + while next_index > self.dag.node_bound() { + // node does not exist + let tmp_node = self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + tmp_nodes.push(tmp_node); + } + // add node to the graph, and update the next available node index + let weight = self.pack_into(py, weight.bind(py))?; + self.dag.add_node(weight); + } + // Remove any temporary nodes we added + for tmp_node in tmp_nodes { + self.dag.remove_node(tmp_node); + } + } + + // to ensure O(1) on edge deletion, use a temporary node to store missing edges + let tmp_node = self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + + for item in edges_lst { + if item.is_none() { + // add a temporary edge that will be deleted later to re-create the hole + self.dag + .add_edge(tmp_node, tmp_node, Wire::Qubit(Qubit(u32::MAX))); + } else { + let triple = item.downcast::().unwrap(); + let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap(); + let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap(); + let edge_w = Wire::from_pickle(&triple.get_item(2).unwrap())?; + self.dag + .add_edge(NodeIndex::new(edge_p), NodeIndex::new(edge_c), edge_w); + } + } + self.dag.remove_node(tmp_node); + Ok(()) + } + + /// Returns the current sequence of registered :class:`.Qubit` instances as a list. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`DAGCircuit` data + /// structures. + /// + /// Returns: + /// list(:class:`.Qubit`): The current sequence of registered qubits. + #[getter] + pub fn qubits(&self, py: Python<'_>) -> Py { + self.qubits.cached().clone_ref(py) + } + + /// Returns the current sequence of registered :class:`.Clbit` + /// instances as a list. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`DAGCircuit` data + /// structures. + /// + /// Returns: + /// list(:class:`.Clbit`): The current sequence of registered clbits. + #[getter] + pub fn clbits(&self, py: Python<'_>) -> Py { + self.clbits.cached().clone_ref(py) + } + + /// Return a list of the wires in order. + #[getter] + fn get_wires(&self, py: Python<'_>) -> PyResult> { + let wires: Vec<&PyObject> = self + .qubits + .bits() + .iter() + .chain(self.clbits.bits().iter()) + .collect(); + let out_list = PyList::new_bound(py, wires); + for var_type_set in &self.vars_by_type { + for var in var_type_set.bind(py).iter() { + out_list.append(var)?; + } + } + Ok(out_list.unbind()) + } + + /// Returns the number of nodes in the dag. + #[getter] + fn get_node_counter(&self) -> usize { + self.dag.node_count() + } + + /// Return the global phase of the circuit. + #[getter] + fn get_global_phase(&self) -> Param { + self.global_phase.clone() + } + + /// Set the global phase of the circuit. + /// + /// Args: + /// angle (float, :class:`.ParameterExpression`): The phase angle. + #[setter] + fn set_global_phase(&mut self, angle: Param) -> PyResult<()> { + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * PI)); + } + Param::ParameterExpression(angle) => { + self.global_phase = Param::ParameterExpression(angle); + } + Param::Obj(_) => return Err(PyTypeError::new_err("Invalid type for global phase")), + } + Ok(()) + } + + /// Return calibration dictionary. + /// + /// The custom pulse definition of a given gate is of the form + /// {'gate_name': {(qubits, params): schedule}} + #[getter] + fn get_calibrations(&self) -> HashMap> { + self.calibrations.clone() + } + + /// Set the circuit calibration data from a dictionary of calibration definition. + /// + /// Args: + /// calibrations (dict): A dictionary of input in the format + /// {'gate_name': {(qubits, gate_params): schedule}} + #[setter] + fn set_calibrations(&mut self, calibrations: HashMap>) { + self.calibrations = calibrations; + } + + /// Register a low-level, custom pulse definition for the given gate. + /// + /// Args: + /// gate (Union[Gate, str]): Gate information. + /// qubits (Union[int, Tuple[int]]): List of qubits to be measured. + /// schedule (Schedule): Schedule information. + /// params (Optional[List[Union[float, Parameter]]]): A list of parameters. + /// + /// Raises: + /// Exception: if the gate is of type string and params is None. + fn add_calibration<'py>( + &mut self, + py: Python<'py>, + mut gate: Bound<'py, PyAny>, + qubits: Bound<'py, PyAny>, + schedule: Py, + mut params: Option>, + ) -> PyResult<()> { + if gate.is_instance(imports::GATE.get_bound(py))? { + params = Some(gate.getattr(intern!(py, "params"))?); + gate = gate.getattr(intern!(py, "name"))?; + } + + let params_tuple = if let Some(operands) = params { + let add_calibration = PyModule::from_code_bound( + py, + r#" +import numpy as np + +def _format(operand): + try: + # Using float/complex value as a dict key is not good idea. + # This makes the mapping quite sensitive to the rounding error. + # However, the mechanism is already tied to the execution model (i.e. pulse gate) + # and we cannot easily update this rule. + # The same logic exists in QuantumCircuit.add_calibration. + evaluated = complex(operand) + if np.isreal(evaluated): + evaluated = float(evaluated.real) + if evaluated.is_integer(): + evaluated = int(evaluated) + return evaluated + except TypeError: + # Unassigned parameter + return operand + "#, + "add_calibration.py", + "add_calibration", + )?; + + let format = add_calibration.getattr("_format")?; + let mapped: PyResult> = operands.iter()?.map(|p| format.call1((p?,))).collect(); + PyTuple::new_bound(py, mapped?).into_any() + } else { + PyTuple::empty_bound(py).into_any() + }; + + let calibrations = self + .calibrations + .entry(gate.extract()?) + .or_insert_with(|| PyDict::new_bound(py).unbind()) + .bind(py); + + let qubits = if let Ok(qubits) = qubits.downcast::() { + qubits.to_tuple()?.into_any() + } else { + PyTuple::new_bound(py, [qubits]).into_any() + }; + let key = PyTuple::new_bound(py, &[qubits.unbind(), params_tuple.into_any().unbind()]); + calibrations.set_item(key, schedule)?; + Ok(()) + } + + /// Return True if the dag has a calibration defined for the node operation. In this + /// case, the operation does not need to be translated to the device basis. + fn has_calibration_for(&self, py: Python, node: PyRef) -> PyResult { + if !self + .calibrations + .contains_key(node.instruction.operation.name()) + { + return Ok(false); + } + let mut params = Vec::new(); + for p in &node.instruction.params { + if let Param::ParameterExpression(exp) = p { + let exp = exp.bind(py); + if !exp.getattr(intern!(py, "parameters"))?.is_truthy()? { + let as_py_float = exp.call_method0(intern!(py, "__float__"))?; + params.push(as_py_float.unbind()); + continue; + } + } + params.push(p.to_object(py)); + } + let qubits: Vec = self + .qubits + .map_bits(node.instruction.qubits.bind(py).iter())? + .map(|bit| bit.0) + .collect(); + let qubits = PyTuple::new_bound(py, qubits); + let params = PyTuple::new_bound(py, params); + self.calibrations[node.instruction.operation.name()] + .bind(py) + .contains((qubits, params).to_object(py)) + } + + /// Remove all operation nodes with the given name. + fn remove_all_ops_named(&mut self, opname: &str) { + let mut to_remove = Vec::new(); + for (id, weight) in self.dag.node_references() { + if let NodeType::Operation(packed) = &weight { + if opname == packed.op.name() { + to_remove.push(id); + } + } + } + for node in to_remove { + self.remove_op_node(node); + } + } + + /// Add individual qubit wires. + fn add_qubits(&mut self, py: Python, qubits: Vec>) -> PyResult<()> { + for bit in qubits.iter() { + if !bit.is_instance(imports::QUBIT.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a Qubit instance.")); + } + + if self.qubits.find(bit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate qubits {}", + bit + ))); + } + } + + for bit in qubits.iter() { + self.add_qubit_unchecked(py, bit)?; + } + Ok(()) + } + + /// Add individual qubit wires. + fn add_clbits(&mut self, py: Python, clbits: Vec>) -> PyResult<()> { + for bit in clbits.iter() { + if !bit.is_instance(imports::CLBIT.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a Clbit instance.")); + } + + if self.clbits.find(bit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate clbits {}", + bit + ))); + } + } + + for bit in clbits.iter() { + self.add_clbit_unchecked(py, bit)?; + } + Ok(()) + } + + /// Add all wires in a quantum register. + fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { + if !qreg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a QuantumRegister instance.")); + } + + let register_name = qreg.getattr(intern!(py, "name"))?; + if self.qregs.bind(py).contains(®ister_name)? { + return Err(DAGCircuitError::new_err(format!( + "duplicate register {}", + register_name + ))); + } + self.qregs.bind(py).set_item(®ister_name, qreg)?; + + for (index, bit) in qreg.iter()?.enumerate() { + let bit = bit?; + if self.qubits.find(&bit).is_none() { + self.add_qubit_unchecked(py, &bit)?; + } + let locations: PyRef = self + .qubit_locations + .bind(py) + .get_item(&bit)? + .unwrap() + .extract()?; + locations.registers.bind(py).append((qreg, index))?; + } + Ok(()) + } + + /// Add all wires in a classical register. + fn add_creg(&mut self, py: Python, creg: &Bound) -> PyResult<()> { + if !creg.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + return Err(DAGCircuitError::new_err( + "not a ClassicalRegister instance.", + )); + } + + let register_name = creg.getattr(intern!(py, "name"))?; + if self.cregs.bind(py).contains(®ister_name)? { + return Err(DAGCircuitError::new_err(format!( + "duplicate register {}", + register_name + ))); + } + self.cregs.bind(py).set_item(register_name, creg)?; + + for (index, bit) in creg.iter()?.enumerate() { + let bit = bit?; + if self.clbits.find(&bit).is_none() { + self.add_clbit_unchecked(py, &bit)?; + } + let locations: PyRef = self + .clbit_locations + .bind(py) + .get_item(&bit)? + .unwrap() + .extract()?; + locations.registers.bind(py).append((creg, index))?; + } + Ok(()) + } + + /// Finds locations in the circuit, by mapping the Qubit and Clbit to positional index + /// BitLocations is defined as: BitLocations = namedtuple("BitLocations", ("index", "registers")) + /// + /// Args: + /// bit (Bit): The bit to locate. + /// + /// Returns: + /// namedtuple(int, List[Tuple(Register, int)]): A 2-tuple. The first element (``index``) + /// contains the index at which the ``Bit`` can be found (in either + /// :obj:`~DAGCircuit.qubits`, :obj:`~DAGCircuit.clbits`, depending on its + /// type). The second element (``registers``) is a list of ``(register, index)`` + /// pairs with an entry for each :obj:`~Register` in the circuit which contains the + /// :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). + /// + /// Raises: + /// DAGCircuitError: If the supplied :obj:`~Bit` was of an unknown type. + /// DAGCircuitError: If the supplied :obj:`~Bit` could not be found on the circuit. + fn find_bit<'py>(&self, py: Python<'py>, bit: &Bound) -> PyResult> { + if bit.is_instance(imports::QUBIT.get_bound(py))? { + return self.qubit_locations.bind(py).get_item(bit)?.ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Could not locate provided bit: {}. Has it been added to the DAGCircuit?", + bit + )) + }); + } + + if bit.is_instance(imports::CLBIT.get_bound(py))? { + return self.clbit_locations.bind(py).get_item(bit)?.ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Could not locate provided bit: {}. Has it been added to the DAGCircuit?", + bit + )) + }); + } + + Err(DAGCircuitError::new_err(format!( + "Could not locate bit of unknown type: {}", + bit.get_type() + ))) + } + + /// Remove classical bits from the circuit. All bits MUST be idle. + /// Any registers with references to at least one of the specified bits will + /// also be removed. + /// + /// .. warning:: + /// This method is rather slow, since it must iterate over the entire + /// DAG to fix-up bit indices. + /// + /// Args: + /// clbits (List[Clbit]): The bits to remove. + /// + /// Raises: + /// DAGCircuitError: a clbit is not a :obj:`.Clbit`, is not in the circuit, + /// or is not idle. + #[pyo3(signature = (*clbits))] + fn remove_clbits(&mut self, py: Python, clbits: &Bound) -> PyResult<()> { + let mut non_bits = Vec::new(); + for bit in clbits.iter() { + if !bit.is_instance(imports::CLBIT.get_bound(py))? { + non_bits.push(bit); + } + } + if !non_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "clbits not of type Clbit: {:?}", + non_bits + ))); + } + + let bit_iter = match self.clbits.map_bits(clbits.iter()) { + Ok(bit_iter) => bit_iter, + Err(_) => { + return Err(DAGCircuitError::new_err(format!( + "clbits not in circuit: {:?}", + clbits + ))) + } + }; + let clbits: HashSet = bit_iter.collect(); + let mut busy_bits = Vec::new(); + for bit in clbits.iter() { + if !self.is_wire_idle(py, &Wire::Clbit(*bit))? { + busy_bits.push(self.clbits.get(*bit).unwrap()); + } + } + + if !busy_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "clbits not idle: {:?}", + busy_bits + ))); + } + + // Remove any references to bits. + let mut cregs_to_remove = Vec::new(); + for creg in self.cregs.bind(py).values() { + for bit in creg.iter()? { + let bit = bit?; + if clbits.contains(&self.clbits.find(&bit).unwrap()) { + cregs_to_remove.push(creg); + break; + } + } + } + self.remove_cregs(py, &PyTuple::new_bound(py, cregs_to_remove))?; + + // Remove DAG in/out nodes etc. + for bit in clbits.iter() { + self.remove_idle_wire(py, Wire::Clbit(*bit))?; + } + + // Copy the current clbit mapping so we can use it while remapping + // wires used on edges and in operation cargs. + let old_clbits = self.clbits.clone(); + + // Remove the clbit indices, which will invalidate our mapping of Clbit to + // Python bits throughout the entire DAG. + self.clbits.remove_indices(py, clbits.clone())?; + + // Update input/output maps to use new Clbits. + let io_mapping: HashMap = self + .clbit_io_map + .drain(..) + .enumerate() + .filter_map(|(k, v)| { + let clbit = Clbit(k as u32); + if clbits.contains(&clbit) { + None + } else { + Some(( + self.clbits + .find(old_clbits.get(Clbit(k as u32)).unwrap().bind(py)) + .unwrap(), + v, + )) + } + }) + .collect(); + + self.clbit_io_map = (0..io_mapping.len()) + .map(|idx| { + let clbit = Clbit(idx as u32); + io_mapping[&clbit] + }) + .collect(); + + // Update edges to use the new Clbits. + for edge_weight in self.dag.edge_weights_mut() { + if let Wire::Clbit(c) = edge_weight { + *c = self + .clbits + .find(old_clbits.get(*c).unwrap().bind(py)) + .unwrap(); + } + } + + // Update operation cargs to use the new Clbits. + for node_weight in self.dag.node_weights_mut() { + match node_weight { + NodeType::Operation(op) => { + let cargs = self.cargs_cache.intern(op.clbits); + let carg_bits = old_clbits + .map_indices(&cargs[..]) + .map(|b| b.bind(py).clone()); + let mapped_cargs = self.clbits.map_bits(carg_bits)?.collect(); + let clbits = Interner::intern(&mut self.cargs_cache, mapped_cargs)?; + op.clbits = clbits; + } + NodeType::ClbitIn(c) | NodeType::ClbitOut(c) => { + *c = self + .clbits + .find(old_clbits.get(*c).unwrap().bind(py)) + .unwrap(); + } + _ => (), + } + } + + // Update bit locations. + let bit_locations = self.clbit_locations.bind(py); + for (i, bit) in self.clbits.bits().iter().enumerate() { + let raw_loc = bit_locations.get_item(bit)?.unwrap(); + let loc = raw_loc.downcast::().unwrap(); + loc.borrow_mut().index = i; + bit_locations.set_item(bit, loc)?; + } + Ok(()) + } + + /// Remove classical registers from the circuit, leaving underlying bits + /// in place. + /// + /// Raises: + /// DAGCircuitError: a creg is not a ClassicalRegister, or is not in + /// the circuit. + #[pyo3(signature = (*cregs))] + fn remove_cregs(&mut self, py: Python, cregs: &Bound) -> PyResult<()> { + let mut non_regs = Vec::new(); + let mut unknown_regs = Vec::new(); + let self_bound_cregs = self.cregs.bind(py); + for reg in cregs.iter() { + if !reg.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + non_regs.push(reg); + } else if let Some(existing_creg) = + self_bound_cregs.get_item(®.getattr(intern!(py, "name"))?)? + { + if !existing_creg.eq(®)? { + unknown_regs.push(reg); + } + } else { + unknown_regs.push(reg); + } + } + if !non_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "cregs not of type ClassicalRegister: {:?}", + non_regs + ))); + } + if !unknown_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "cregs not in circuit: {:?}", + unknown_regs + ))); + } + + for creg in cregs { + self.cregs + .bind(py) + .del_item(creg.getattr(intern!(py, "name"))?)?; + for (i, bit) in creg.iter()?.enumerate() { + let bit = bit?; + let bit_position = self + .clbit_locations + .bind(py) + .get_item(bit)? + .unwrap() + .downcast_into_exact::()?; + bit_position + .borrow() + .registers + .bind(py) + .as_any() + .call_method1(intern!(py, "remove"), ((&creg, i),))?; + } + } + Ok(()) + } + + /// Remove quantum bits from the circuit. All bits MUST be idle. + /// Any registers with references to at least one of the specified bits will + /// also be removed. + /// + /// .. warning:: + /// This method is rather slow, since it must iterate over the entire + /// DAG to fix-up bit indices. + /// + /// Args: + /// qubits (List[~qiskit.circuit.Qubit]): The bits to remove. + /// + /// Raises: + /// DAGCircuitError: a qubit is not a :obj:`~.circuit.Qubit`, is not in the circuit, + /// or is not idle. + #[pyo3(signature = (*qubits))] + fn remove_qubits(&mut self, py: Python, qubits: &Bound) -> PyResult<()> { + let mut non_qbits = Vec::new(); + for bit in qubits.iter() { + if !bit.is_instance(imports::QUBIT.get_bound(py))? { + non_qbits.push(bit); + } + } + if !non_qbits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qubits not of type Qubit: {:?}", + non_qbits + ))); + } + + let bit_iter = match self.qubits.map_bits(qubits.iter()) { + Ok(bit_iter) => bit_iter, + Err(_) => { + return Err(DAGCircuitError::new_err(format!( + "qubits not in circuit: {:?}", + qubits + ))) + } + }; + let qubits: HashSet = bit_iter.collect(); + + let mut busy_bits = Vec::new(); + for bit in qubits.iter() { + if !self.is_wire_idle(py, &Wire::Qubit(*bit))? { + busy_bits.push(self.qubits.get(*bit).unwrap()); + } + } + + if !busy_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qubits not idle: {:?}", + busy_bits + ))); + } + + // Remove any references to bits. + let mut qregs_to_remove = Vec::new(); + for qreg in self.qregs.bind(py).values() { + for bit in qreg.iter()? { + let bit = bit?; + if qubits.contains(&self.qubits.find(&bit).unwrap()) { + qregs_to_remove.push(qreg); + break; + } + } + } + self.remove_qregs(py, &PyTuple::new_bound(py, qregs_to_remove))?; + + // Remove DAG in/out nodes etc. + for bit in qubits.iter() { + self.remove_idle_wire(py, Wire::Qubit(*bit))?; + } + + // Copy the current qubit mapping so we can use it while remapping + // wires used on edges and in operation qargs. + let old_qubits = self.qubits.clone(); + + // Remove the qubit indices, which will invalidate our mapping of Qubit to + // Python bits throughout the entire DAG. + self.qubits.remove_indices(py, qubits.clone())?; + + // Update input/output maps to use new Qubits. + let io_mapping: HashMap = self + .qubit_io_map + .drain(..) + .enumerate() + .filter_map(|(k, v)| { + let qubit = Qubit(k as u32); + if qubits.contains(&qubit) { + None + } else { + Some(( + self.qubits + .find(old_qubits.get(qubit).unwrap().bind(py)) + .unwrap(), + v, + )) + } + }) + .collect(); + + self.qubit_io_map = (0..io_mapping.len()) + .map(|idx| { + let qubit = Qubit(idx as u32); + io_mapping[&qubit] + }) + .collect(); + + // Update edges to use the new Qubits. + for edge_weight in self.dag.edge_weights_mut() { + if let Wire::Qubit(b) = edge_weight { + *b = self + .qubits + .find(old_qubits.get(*b).unwrap().bind(py)) + .unwrap(); + } + } + + // Update operation qargs to use the new Qubits. + for node_weight in self.dag.node_weights_mut() { + match node_weight { + NodeType::Operation(op) => { + let qargs = self.qargs_cache.intern(op.qubits); + let qarg_bits = old_qubits + .map_indices(&qargs[..]) + .map(|b| b.bind(py).clone()); + let mapped_qargs = self.qubits.map_bits(qarg_bits)?.collect(); + let qubits = Interner::intern(&mut self.qargs_cache, mapped_qargs)?; + op.qubits = qubits; + } + NodeType::QubitIn(q) | NodeType::QubitOut(q) => { + *q = self + .qubits + .find(old_qubits.get(*q).unwrap().bind(py)) + .unwrap(); + } + _ => (), + } + } + + // Update bit locations. + let bit_locations = self.qubit_locations.bind(py); + for (i, bit) in self.qubits.bits().iter().enumerate() { + let raw_loc = bit_locations.get_item(bit)?.unwrap(); + let loc = raw_loc.downcast::().unwrap(); + loc.borrow_mut().index = i; + bit_locations.set_item(bit, loc)?; + } + Ok(()) + } + + /// Remove quantum registers from the circuit, leaving underlying bits + /// in place. + /// + /// Raises: + /// DAGCircuitError: a qreg is not a QuantumRegister, or is not in + /// the circuit. + #[pyo3(signature = (*qregs))] + fn remove_qregs(&mut self, py: Python, qregs: &Bound) -> PyResult<()> { + let mut non_regs = Vec::new(); + let mut unknown_regs = Vec::new(); + let self_bound_qregs = self.qregs.bind(py); + for reg in qregs.iter() { + if !reg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { + non_regs.push(reg); + } else if let Some(existing_qreg) = + self_bound_qregs.get_item(®.getattr(intern!(py, "name"))?)? + { + if !existing_qreg.eq(®)? { + unknown_regs.push(reg); + } + } else { + unknown_regs.push(reg); + } + } + if !non_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qregs not of type QuantumRegister: {:?}", + non_regs + ))); + } + if !unknown_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qregs not in circuit: {:?}", + unknown_regs + ))); + } + + for qreg in qregs { + self.qregs + .bind(py) + .del_item(qreg.getattr(intern!(py, "name"))?)?; + for (i, bit) in qreg.iter()?.enumerate() { + let bit = bit?; + let bit_position = self + .qubit_locations + .bind(py) + .get_item(bit)? + .unwrap() + .downcast_into_exact::()?; + bit_position + .borrow() + .registers + .bind(py) + .as_any() + .call_method1(intern!(py, "remove"), ((&qreg, i),))?; + } + } + Ok(()) + } + + /// Verify that the condition is valid. + /// + /// Args: + /// name (string): used for error reporting + /// condition (tuple or None): a condition tuple (ClassicalRegister, int) or (Clbit, bool) + /// + /// Raises: + /// DAGCircuitError: if conditioning on an invalid register + fn _check_condition(&self, py: Python, name: &str, condition: &Bound) -> PyResult<()> { + if condition.is_none() { + return Ok(()); + } + + let resources = self.control_flow_module.condition_resources(condition)?; + for reg in resources.cregs.bind(py) { + if !self + .cregs + .bind(py) + .contains(reg.getattr(intern!(py, "name"))?)? + { + return Err(DAGCircuitError::new_err(format!( + "invalid creg in condition for {}", + name + ))); + } + } + + for bit in resources.clbits.bind(py) { + if self.clbits.find(&bit).is_none() { + return Err(DAGCircuitError::new_err(format!( + "invalid clbits in condition for {}", + name + ))); + } + } + + Ok(()) + } + + /// Return a copy of self with the same structure but empty. + /// + /// That structure includes: + /// * name and other metadata + /// * global phase + /// * duration + /// * all the qubits and clbits, including the registers. + /// + /// Returns: + /// DAGCircuit: An empty copy of self. + #[pyo3(signature = (*, vars_mode="alike"))] + fn copy_empty_like(&self, py: Python, vars_mode: &str) -> PyResult { + let mut target_dag = DAGCircuit::new(py)?; + target_dag.name = self.name.as_ref().map(|n| n.clone_ref(py)); + target_dag.global_phase = self.global_phase.clone(); + target_dag.duration = self.duration.as_ref().map(|d| d.clone_ref(py)); + target_dag.unit.clone_from(&self.unit); + target_dag.metadata = self.metadata.as_ref().map(|m| m.clone_ref(py)); + target_dag.qargs_cache = self.qargs_cache.clone(); + target_dag.cargs_cache = self.cargs_cache.clone(); + + for bit in self.qubits.bits() { + target_dag.add_qubit_unchecked(py, bit.bind(py))?; + } + for bit in self.clbits.bits() { + target_dag.add_clbit_unchecked(py, bit.bind(py))?; + } + for reg in self.qregs.bind(py).values() { + target_dag.add_qreg(py, ®)?; + } + for reg in self.cregs.bind(py).values() { + target_dag.add_creg(py, ®)?; + } + if vars_mode == "alike" { + for var in self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Input)?; + } + for var in self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Declare)?; + } + } else if vars_mode == "captures" { + for var in self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + } else if vars_mode != "drop" { + return Err(PyValueError::new_err(format!( + "unknown vars_mode: '{}'", + vars_mode + ))); + } + + Ok(target_dag) + } + + #[pyo3(signature=(node, check=false))] + fn _apply_op_node_back( + &mut self, + py: Python, + node: &Bound, + check: bool, + ) -> PyResult<()> { + if let NodeType::Operation(inst) = self.pack_into(py, node)? { + if check { + self.check_op_addition(py, &inst)?; + } + + self.push_back(py, inst)?; + Ok(()) + } else { + Err(PyTypeError::new_err("Invalid node type input")) + } + } + + /// Apply an operation to the output of the circuit. + /// + /// Args: + /// op (qiskit.circuit.Operation): the operation associated with the DAG node + /// qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to + /// cargs (tuple[Clbit]): cbits that op will be applied to + /// check (bool): If ``True`` (default), this function will enforce that the + /// :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are + /// :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* + /// uphold these invariants itself, but the cost of several checks will be skipped. + /// This is most useful when building a new DAG from a source of known-good nodes. + /// Returns: + /// DAGOpNode: the node for the op that was added to the dag + /// + /// Raises: + /// DAGCircuitError: if a leaf node is connected to multiple outputs + #[pyo3(name = "apply_operation_back", signature = (op, qargs=None, cargs=None, *, check=true))] + fn py_apply_operation_back( + &mut self, + py: Python, + op: Bound, + qargs: Option, + cargs: Option, + check: bool, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map(|q| q.value); + let cargs = cargs.map(|c| c.value); + let node = { + let qubits_id = Interner::intern( + &mut self.qargs_cache, + self.qubits.map_bits(qargs.iter().flatten())?.collect(), + )?; + let clbits_id = Interner::intern( + &mut self.cargs_cache, + self.clbits.map_bits(cargs.iter().flatten())?.collect(), + )?; + let instr = PackedInstruction { + op: py_op.operation, + qubits: qubits_id, + clbits: clbits_id, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + if check { + self.check_op_addition(py, &instr)?; + } + self.push_back(py, instr)? + }; + + self.get_node(py, node) + } + + /// Apply an operation to the input of the circuit. + /// + /// Args: + /// op (qiskit.circuit.Operation): the operation associated with the DAG node + /// qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to + /// cargs (tuple[Clbit]): cbits that op will be applied to + /// check (bool): If ``True`` (default), this function will enforce that the + /// :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are + /// :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* + /// uphold these invariants itself, but the cost of several checks will be skipped. + /// This is most useful when building a new DAG from a source of known-good nodes. + /// Returns: + /// DAGOpNode: the node for the op that was added to the dag + /// + /// Raises: + /// DAGCircuitError: if initial nodes connected to multiple out edges + #[pyo3(name = "apply_operation_front", signature = (op, qargs=None, cargs=None, *, check=true))] + fn py_apply_operation_front( + &mut self, + py: Python, + op: Bound, + qargs: Option, + cargs: Option, + check: bool, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map(|q| q.value); + let cargs = cargs.map(|c| c.value); + let node = { + let qubits_id = Interner::intern( + &mut self.qargs_cache, + self.qubits.map_bits(qargs.iter().flatten())?.collect(), + )?; + let clbits_id = Interner::intern( + &mut self.cargs_cache, + self.clbits.map_bits(cargs.iter().flatten())?.collect(), + )?; + let instr = PackedInstruction { + op: py_op.operation, + qubits: qubits_id, + clbits: clbits_id, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + if check { + self.check_op_addition(py, &instr)?; + } + self.push_front(py, instr)? + }; + + self.get_node(py, node) + } + + /// Compose the ``other`` circuit onto the output of this circuit. + /// + /// A subset of input wires of ``other`` are mapped + /// to a subset of output wires of this circuit. + /// + /// ``other`` can be narrower or of equal width to ``self``. + /// + /// Args: + /// other (DAGCircuit): circuit to compose with self + /// qubits (list[~qiskit.circuit.Qubit|int]): qubits of self to compose onto. + /// clbits (list[Clbit|int]): clbits of self to compose onto. + /// front (bool): If True, front composition will be performed (not implemented yet) + /// inplace (bool): If True, modify the object. Otherwise return composed circuit. + /// inline_captures (bool): If ``True``, variables marked as "captures" in the ``other`` DAG + /// will be inlined onto existing uses of those same variables in ``self``. If ``False``, + /// all variables in ``other`` are required to be distinct from ``self``, and they will + /// be added to ``self``. + /// + /// .. + /// Note: unlike `QuantumCircuit.compose`, there's no `var_remap` argument here. That's + /// because the `DAGCircuit` inner-block structure isn't set up well to allow the recursion, + /// and `DAGCircuit.compose` is generally only used to rebuild a DAG from layers within + /// itself than to join unrelated circuits. While there's no strong motivating use-case + /// (unlike the `QuantumCircuit` equivalent), it's safer and more performant to not provide + /// the option. + /// + /// Returns: + /// DAGCircuit: the composed dag (returns None if inplace==True). + /// + /// Raises: + /// DAGCircuitError: if ``other`` is wider or there are duplicate edge mappings. + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (other, qubits=None, clbits=None, front=false, inplace=true, *, inline_captures=false))] + fn compose( + slf: PyRefMut, + py: Python, + other: &DAGCircuit, + qubits: Option>, + clbits: Option>, + front: bool, + inplace: bool, + inline_captures: bool, + ) -> PyResult> { + if front { + return Err(DAGCircuitError::new_err( + "Front composition not supported yet.", + )); + } + + if other.qubits.len() > slf.qubits.len() || other.clbits.len() > slf.clbits.len() { + return Err(DAGCircuitError::new_err( + "Trying to compose with another DAGCircuit which has more 'in' edges.", + )); + } + + // Number of qubits and clbits must match number in circuit or None + let identity_qubit_map = other + .qubits + .bits() + .iter() + .zip(slf.qubits.bits()) + .into_py_dict_bound(py); + let identity_clbit_map = other + .clbits + .bits() + .iter() + .zip(slf.clbits.bits()) + .into_py_dict_bound(py); + + let qubit_map: Bound = match qubits { + None => identity_qubit_map.clone(), + Some(qubits) => { + if qubits.len() != other.qubits.len() { + return Err(DAGCircuitError::new_err(concat!( + "Number of items in qubits parameter does not", + " match number of qubits in the circuit." + ))); + } + + let self_qubits = slf.qubits.cached().bind(py); + let other_qubits = other.qubits.cached().bind(py); + let dict = PyDict::new_bound(py); + for (i, q) in qubits.iter().enumerate() { + let q = if q.is_instance_of::() { + self_qubits.get_item(q.extract()?)? + } else { + q + }; + + dict.set_item(other_qubits.get_item(i)?, q)?; + } + dict + } + }; + + let clbit_map: Bound = match clbits { + None => identity_clbit_map.clone(), + Some(clbits) => { + if clbits.len() != other.clbits.len() { + return Err(DAGCircuitError::new_err(concat!( + "Number of items in clbits parameter does not", + " match number of clbits in the circuit." + ))); + } + + let self_clbits = slf.clbits.cached().bind(py); + let other_clbits = other.clbits.cached().bind(py); + let dict = PyDict::new_bound(py); + for (i, q) in clbits.iter().enumerate() { + let q = if q.is_instance_of::() { + self_clbits.get_item(q.extract()?)? + } else { + q + }; + + dict.set_item(other_clbits.get_item(i)?, q)?; + } + dict + } + }; + + let edge_map = if qubit_map.is_empty() && clbit_map.is_empty() { + // try to do a 1-1 mapping in order + identity_qubit_map + .iter() + .chain(identity_clbit_map.iter()) + .into_py_dict_bound(py) + } else { + qubit_map + .iter() + .chain(clbit_map.iter()) + .into_py_dict_bound(py) + }; + + // Chck duplicates in wire map. + { + let edge_map_values: Vec<_> = edge_map.values().iter().collect(); + if PySet::new_bound(py, edge_map_values.as_slice())?.len() != edge_map.len() { + return Err(DAGCircuitError::new_err("duplicates in wire_map")); + } + } + + // Compose + let mut dag: PyRefMut = if inplace { + slf + } else { + Py::new(py, slf.clone())?.into_bound(py).borrow_mut() + }; + + dag.global_phase = add_global_phase(py, &dag.global_phase, &other.global_phase)?; + + for (gate, cals) in other.calibrations.iter() { + let calibrations = match dag.calibrations.get(gate) { + Some(calibrations) => calibrations, + None => { + dag.calibrations + .insert(gate.clone(), PyDict::new_bound(py).unbind()); + &dag.calibrations[gate] + } + }; + calibrations.bind(py).update(cals.bind(py).as_mapping())?; + } + + // This is all the handling we need for realtime variables, if there's no remapping. They: + // + // * get added to the DAG and then operations involving them get appended on normally. + // * get inlined onto an existing variable, then operations get appended normally. + // * there's a clash or a failed inlining, and we just raise an error. + // + // Notably if there's no remapping, there's no need to recurse into control-flow or to do any + // Var rewriting during the Expr visits. + for var in other.iter_input_vars(py)?.bind(py) { + dag.add_input_var(py, &var?)?; + } + if inline_captures { + for var in other.iter_captured_vars(py)?.bind(py) { + let var = var?; + if !dag.has_var(&var)? { + return Err(DAGCircuitError::new_err(format!("Variable '{}' to be inlined is not in the base DAG. If you wanted it to be automatically added, use `inline_captures=False`.", var))); + } + } + } else { + for var in other.iter_captured_vars(py)?.bind(py) { + dag.add_captured_var(py, &var?)?; + } + } + for var in other.iter_declared_vars(py)?.bind(py) { + dag.add_declared_var(py, &var?)?; + } + + let variable_mapper = PyVariableMapper::new( + py, + dag.cregs.bind(py).values().into_any(), + Some(edge_map.clone()), + None, + Some(wrap_pyfunction_bound!(reject_new_register, py)?.to_object(py)), + )?; + + for node in other.topological_nodes()? { + match &other.dag[node] { + NodeType::QubitIn(q) => { + let bit = other.qubits.get(*q).unwrap().bind(py); + let m_wire = edge_map.get_item(bit)?.unwrap_or_else(|| bit.clone()); + let wire_in_dag = dag.qubits.find(&m_wire); + + if wire_in_dag.is_none() + || (dag.qubit_io_map.len() - 1 < wire_in_dag.unwrap().0 as usize) + { + return Err(DAGCircuitError::new_err(format!( + "wire {} not in self", + m_wire, + ))); + } + // TODO: Python code has check here if node.wire is in other._wires. Why? + } + NodeType::ClbitIn(c) => { + let bit = other.clbits.get(*c).unwrap().bind(py); + let m_wire = edge_map.get_item(bit)?.unwrap_or_else(|| bit.clone()); + let wire_in_dag = dag.clbits.find(&m_wire); + if wire_in_dag.is_none() + || dag.clbit_io_map.len() - 1 < wire_in_dag.unwrap().0 as usize + { + return Err(DAGCircuitError::new_err(format!( + "wire {} not in self", + m_wire, + ))); + } + // TODO: Python code has check here if node.wire is in other._wires. Why? + } + NodeType::Operation(op) => { + let m_qargs = { + let qubits = other + .qubits + .map_indices(other.qargs_cache.intern(op.qubits).as_slice()); + let mut mapped = Vec::with_capacity(qubits.len()); + for bit in qubits { + mapped.push( + edge_map + .get_item(bit)? + .unwrap_or_else(|| bit.bind(py).clone()), + ); + } + PyTuple::new_bound(py, mapped) + }; + let m_cargs = { + let clbits = other + .clbits + .map_indices(other.cargs_cache.intern(op.clbits).as_slice()); + let mut mapped = Vec::with_capacity(clbits.len()); + for bit in clbits { + mapped.push( + edge_map + .get_item(bit)? + .unwrap_or_else(|| bit.bind(py).clone()), + ); + } + PyTuple::new_bound(py, mapped) + }; + + // We explicitly create a mutable py_op here since we might + // update the condition. + let mut py_op = op.unpack_py_op(py)?.into_bound(py); + if py_op.getattr(intern!(py, "mutable"))?.extract::()? { + py_op = py_op.call_method0(intern!(py, "to_mutable"))?; + } + + if let Some(condition) = op.condition() { + // TODO: do we need to check for condition.is_none()? + let condition = variable_mapper.map_condition(condition.bind(py), true)?; + if !op.op.control_flow() { + py_op = py_op.call_method1( + intern!(py, "c_if"), + condition.downcast::()?, + )?; + } else { + py_op.setattr(intern!(py, "condition"), condition)?; + } + } else if py_op.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { + py_op.setattr( + intern!(py, "target"), + variable_mapper.map_target(&py_op.getattr(intern!(py, "target"))?)?, + )?; + }; + + dag.py_apply_operation_back( + py, + py_op, + Some(TupleLikeArg { value: m_qargs }), + Some(TupleLikeArg { value: m_cargs }), + false, + )?; + } + // If its a Var wire, we already checked that it exists in the destination. + NodeType::VarIn(_) + | NodeType::VarOut(_) + | NodeType::QubitOut(_) + | NodeType::ClbitOut(_) => (), + } + } + + if !inplace { + Ok(Some(dag.into_py(py))) + } else { + Ok(None) + } + } + + /// Reverse the operations in the ``self`` circuit. + /// + /// Returns: + /// DAGCircuit: the reversed dag. + fn reverse_ops<'py>(slf: PyRef, py: Python<'py>) -> PyResult> { + let qc = imports::DAG_TO_CIRCUIT.get_bound(py).call1((slf,))?; + let reversed = qc.call_method0("reverse_ops")?; + imports::CIRCUIT_TO_DAG.get_bound(py).call1((reversed,)) + } + + /// Return idle wires. + /// + /// Args: + /// ignore (list(str)): List of node names to ignore. Default: [] + /// + /// Yields: + /// Bit: Bit in idle wire. + /// + /// Raises: + /// DAGCircuitError: If the DAG is invalid + fn idle_wires(&self, py: Python, ignore: Option<&Bound>) -> PyResult> { + let mut result: Vec = Vec::new(); + let wires = (0..self.qubit_io_map.len()) + .map(|idx| Wire::Qubit(Qubit(idx as u32))) + .chain((0..self.clbit_io_map.len()).map(|idx| Wire::Clbit(Clbit(idx as u32)))) + .chain(self.var_input_map.keys(py).map(Wire::Var)); + match ignore { + Some(ignore) => { + // Convert the list to a Rust set. + let ignore_set = ignore + .into_iter() + .map(|s| s.extract()) + .collect::>>()?; + for wire in wires { + let nodes_found = self.nodes_on_wire(py, &wire, true).into_iter().any(|node| { + let weight = self.dag.node_weight(node).unwrap(); + if let NodeType::Operation(packed) = weight { + !ignore_set.contains(packed.op.name()) + } else { + false + } + }); + + if !nodes_found { + result.push(match wire { + Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), + Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), + Wire::Var(var) => var, + }); + } + } + } + None => { + for wire in wires { + if self.is_wire_idle(py, &wire)? { + result.push(match wire { + Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), + Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), + Wire::Var(var) => var, + }); + } + } + } + } + Ok(PyTuple::new_bound(py, result).into_any().iter()?.unbind()) + } + + /// Return the number of operations. If there is control flow present, this count may only + /// be an estimate, as the complete control-flow path cannot be statically known. + /// + /// Args: + /// recurse: if ``True``, then recurse into control-flow operations. For loops with + /// known-length iterators are counted unrolled. If-else blocks sum both of the two + /// branches. While loops are counted as if the loop body runs once only. Defaults to + /// ``False`` and raises :class:`.DAGCircuitError` if any control flow is present, to + /// avoid silently returning a mostly meaningless number. + /// + /// Returns: + /// int: the circuit size + /// + /// Raises: + /// DAGCircuitError: if an unknown :class:`.ControlFlowOp` is present in a call with + /// ``recurse=True``, or any control flow is present in a non-recursive call. + #[pyo3(signature= (*, recurse=false))] + fn size(&self, py: Python, recurse: bool) -> PyResult { + let mut length = self.dag.node_count() - (self.width() * 2); + if !recurse { + if CONTROL_FLOW_OP_NAMES + .iter() + .any(|n| self.op_names.contains_key(&n.to_string())) + { + return Err(DAGCircuitError::new_err(concat!( + "Size with control flow is ambiguous.", + " You may use `recurse=True` to get a result", + " but see this method's documentation for the meaning of this." + ))); + } + return Ok(length); + } + + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + for node_index in + self.op_nodes_by_py_type(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?, true) + { + let NodeType::Operation(node) = &self.dag[node_index] else { + return Err(DAGCircuitError::new_err("unknown control-flow type")); + }; + let OperationRef::Instruction(inst) = node.op.view() else { + unreachable!("Control Flow operations must be a PyInstruction"); + }; + let inst_bound = inst.instruction.bind(py); + if inst_bound.is_instance(imports::FOR_LOOP_OP.get_bound(py))? { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks: &Bound = raw_blocks.downcast()?; + let block_zero = blocks.get_item(0).unwrap(); + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block_zero.clone(),))?.extract()?; + length += node.params_view().len() * inner_dag.size(py, true)? + } else if inst_bound.is_instance(imports::WHILE_LOOP_OP.get_bound(py))? { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks: &Bound = raw_blocks.downcast()?; + let block_zero = blocks.get_item(0).unwrap(); + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block_zero.clone(),))?.extract()?; + length += inner_dag.size(py, true)? + } else if inst_bound.is_instance(imports::IF_ELSE_OP.get_bound(py))? + || inst_bound.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? + { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks: &Bound = raw_blocks.downcast()?; + for block in blocks.iter() { + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block.clone(),))?.extract()?; + length += inner_dag.size(py, true)?; + } + } else { + continue; + } + // We don't count a control-flow node itself! + length -= 1; + } + Ok(length) + } + + /// Return the circuit depth. If there is control flow present, this count may only be an + /// estimate, as the complete control-flow path cannot be statically known. + /// + /// Args: + /// recurse: if ``True``, then recurse into control-flow operations. For loops + /// with known-length iterators are counted as if the loop had been manually unrolled + /// (*i.e.* with each iteration of the loop body written out explicitly). + /// If-else blocks take the longer case of the two branches. While loops are counted as + /// if the loop body runs once only. Defaults to ``False`` and raises + /// :class:`.DAGCircuitError` if any control flow is present, to avoid silently + /// returning a nonsensical number. + /// + /// Returns: + /// int: the circuit depth + /// + /// Raises: + /// DAGCircuitError: if not a directed acyclic graph + /// DAGCircuitError: if unknown control flow is present in a recursive call, or any control + /// flow is present in a non-recursive call. + #[pyo3(signature= (*, recurse=false))] + fn depth(&self, py: Python, recurse: bool) -> PyResult { + if self.qubits.is_empty() && self.clbits.is_empty() && self.vars_info.is_empty() { + return Ok(0); + } + + Ok(if recurse { + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + let mut node_lookup: HashMap = HashMap::new(); + + for node_index in + self.op_nodes_by_py_type(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?, true) + { + if let NodeType::Operation(node) = &self.dag[node_index] { + if let OperationRef::Instruction(inst) = node.op.view() { + let inst_bound = inst.instruction.bind(py); + let weight = + if inst_bound.is_instance(imports::FOR_LOOP_OP.get_bound(py))? { + node.params_view().len() + } else { + 1 + }; + if weight == 0 { + node_lookup.insert(node_index, 0); + } else { + let raw_blocks = inst_bound.getattr("blocks")?; + let blocks = raw_blocks.downcast::()?; + let mut block_weights: Vec = Vec::with_capacity(blocks.len()); + for block in blocks.iter() { + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block,))?.extract()?; + block_weights.push(inner_dag.depth(py, true)?); + } + node_lookup + .insert(node_index, weight * block_weights.iter().max().unwrap()); + } + } + } + } + + let weight_fn = |edge: EdgeReference<'_, Wire>| -> Result { + Ok(*node_lookup.get(&edge.target()).unwrap_or(&1)) + }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.1, + None => return Err(DAGCircuitError::new_err("not a DAG")), + } + } else { + if CONTROL_FLOW_OP_NAMES + .iter() + .any(|x| self.op_names.contains_key(&x.to_string())) + { + return Err(DAGCircuitError::new_err("Depth with control flow is ambiguous. You may use `recurse=True` to get a result, but see this method's documentation for the meaning of this.")); + } + + let weight_fn = |_| -> Result { Ok(1) }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.1, + None => return Err(DAGCircuitError::new_err("not a DAG")), + } + } - 1) + } + + /// Return the total number of qubits + clbits used by the circuit. + /// This function formerly returned the number of qubits by the calculation + /// return len(self._wires) - self.num_clbits() + /// but was changed by issue #2564 to return number of qubits + clbits + /// with the new function DAGCircuit.num_qubits replacing the former + /// semantic of DAGCircuit.width(). + fn width(&self) -> usize { + self.qubits.len() + self.clbits.len() + self.vars_info.len() + } + + /// Return the total number of qubits used by the circuit. + /// num_qubits() replaces former use of width(). + /// DAGCircuit.width() now returns qubits + clbits for + /// consistency with Circuit.width() [qiskit-terra #2564]. + pub fn num_qubits(&self) -> usize { + self.qubits.len() + } + + /// Return the total number of classical bits used by the circuit. + pub fn num_clbits(&self) -> usize { + self.clbits.len() + } + + /// Compute how many components the circuit can decompose into. + fn num_tensor_factors(&self) -> usize { + // This function was forked from rustworkx's + // number_weekly_connected_components() function as of 0.15.0: + // https://github.com/Qiskit/rustworkx/blob/0.15.0/src/connectivity/mod.rs#L215-L235 + + let mut weak_components = self.dag.node_count(); + let mut vertex_sets = UnionFind::new(self.dag.node_bound()); + for edge in self.dag.edge_references() { + let (a, b) = (edge.source(), edge.target()); + // union the two vertices of the edge + if vertex_sets.union(a.index(), b.index()) { + weak_components -= 1 + }; + } + weak_components + } + + fn __eq__(&self, py: Python, other: &DAGCircuit) -> PyResult { + // Try to convert to float, but in case of unbound ParameterExpressions + // a TypeError will be raise, fallback to normal equality in those + // cases. + let phase_is_close = |self_phase: f64, other_phase: f64| -> bool { + ((self_phase - other_phase + PI).rem_euclid(2. * PI) - PI).abs() <= 1.0e-10 + }; + let normalize_param = |param: &Param| { + if let Param::ParameterExpression(ob) = param { + ob.bind(py) + .call_method0(intern!(py, "numeric")) + .ok() + .map(|ob| ob.extract::()) + .unwrap_or_else(|| Ok(param.clone())) + } else { + Ok(param.clone()) + } + }; + + let phase_eq = match [ + normalize_param(&self.global_phase)?, + normalize_param(&other.global_phase)?, + ] { + [Param::Float(self_phase), Param::Float(other_phase)] => { + Ok(phase_is_close(self_phase, other_phase)) + } + _ => self.global_phase.eq(py, &other.global_phase), + }?; + if !phase_eq { + return Ok(false); + } + if self.calibrations.len() != other.calibrations.len() { + return Ok(false); + } + + for (k, v1) in &self.calibrations { + match other.calibrations.get(k) { + Some(v2) => { + if !v1.bind(py).eq(v2.bind(py))? { + return Ok(false); + } + } + None => { + return Ok(false); + } + } + } + + // We don't do any semantic equivalence between Var nodes, as things stand; DAGs can only be + // equal in our mind if they use the exact same UUID vars. + for (our_vars, their_vars) in self.vars_by_type.iter().zip(&other.vars_by_type) { + if !our_vars.bind(py).eq(their_vars)? { + return Ok(false); + } + } + + let self_bit_indices = { + let indices = self + .qubits + .bits() + .iter() + .chain(self.clbits.bits()) + .enumerate() + .map(|(idx, bit)| (bit, idx)); + indices.into_py_dict_bound(py) + }; + + let other_bit_indices = { + let indices = other + .qubits + .bits() + .iter() + .chain(other.clbits.bits()) + .enumerate() + .map(|(idx, bit)| (bit, idx)); + indices.into_py_dict_bound(py) + }; + + // Check if qregs are the same. + let self_qregs = self.qregs.bind(py); + let other_qregs = other.qregs.bind(py); + if self_qregs.len() != other_qregs.len() { + return Ok(false); + } + for (regname, self_bits) in self_qregs { + let self_bits = self_bits + .getattr("_bits")? + .downcast_into_exact::()?; + let other_bits = match other_qregs.get_item(regname)? { + Some(bits) => bits.getattr("_bits")?.downcast_into_exact::()?, + None => return Ok(false), + }; + if !self + .qubits + .map_bits(self_bits)? + .eq(other.qubits.map_bits(other_bits)?) + { + return Ok(false); + } + } + + // Check if cregs are the same. + let self_cregs = self.cregs.bind(py); + let other_cregs = other.cregs.bind(py); + if self_cregs.len() != other_cregs.len() { + return Ok(false); + } + + for (regname, self_bits) in self_cregs { + let self_bits = self_bits + .getattr("_bits")? + .downcast_into_exact::()?; + let other_bits = match other_cregs.get_item(regname)? { + Some(bits) => bits.getattr("_bits")?.downcast_into_exact::()?, + None => return Ok(false), + }; + if !self + .clbits + .map_bits(self_bits)? + .eq(other.clbits.map_bits(other_bits)?) + { + return Ok(false); + } + } + + // Check for VF2 isomorphic match. + let legacy_condition_eq = imports::LEGACY_CONDITION_CHECK.get_bound(py); + let condition_op_check = imports::CONDITION_OP_CHECK.get_bound(py); + let switch_case_op_check = imports::SWITCH_CASE_OP_CHECK.get_bound(py); + let for_loop_op_check = imports::FOR_LOOP_OP_CHECK.get_bound(py); + let node_match = |n1: &NodeType, n2: &NodeType| -> PyResult { + match [n1, n2] { + [NodeType::Operation(inst1), NodeType::Operation(inst2)] => { + if inst1.op.name() != inst2.op.name() { + return Ok(false); + } + let check_args = || -> bool { + let node1_qargs = self.qargs_cache.intern(inst1.qubits); + let node2_qargs = other.qargs_cache.intern(inst2.qubits); + let node1_cargs = self.cargs_cache.intern(inst1.clbits); + let node2_cargs = other.cargs_cache.intern(inst2.clbits); + if SEMANTIC_EQ_SYMMETRIC.contains(&inst1.op.name()) { + let node1_qargs = + node1_qargs.iter().copied().collect::>(); + let node2_qargs = + node2_qargs.iter().copied().collect::>(); + let node1_cargs = + node1_cargs.iter().copied().collect::>(); + let node2_cargs = + node2_cargs.iter().copied().collect::>(); + if node1_qargs != node2_qargs || node1_cargs != node2_cargs { + return false; + } + } else if node1_qargs != node2_qargs || node1_cargs != node2_cargs { + return false; + } + true + }; + let check_conditions = || -> PyResult { + if let Some(cond1) = inst1 + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + if let Some(cond2) = inst2 + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + legacy_condition_eq + .call1((cond1, cond2, &self_bit_indices, &other_bit_indices))? + .extract::() + } else { + Ok(false) + } + } else { + Ok(inst2 + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + .is_none()) + } + }; + + match [inst1.op.view(), inst2.op.view()] { + [OperationRef::Standard(_op1), OperationRef::Standard(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? + && check_args() + && check_conditions()? + && inst1 + .params_view() + .iter() + .zip(inst2.params_view().iter()) + .all(|(a, b)| a.is_close(py, b, 1e-10).unwrap())) + } + [OperationRef::Instruction(op1), OperationRef::Instruction(op2)] => { + if op1.control_flow() && op2.control_flow() { + let n1 = self.unpack_into(py, NodeIndex::new(0), n1)?; + let n2 = other.unpack_into(py, NodeIndex::new(0), n2)?; + let name = op1.name(); + if name == "if_else" || name == "while_loop" { + condition_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else if name == "switch_case" { + switch_case_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else if name == "for_loop" { + for_loop_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else { + Err(PyRuntimeError::new_err(format!( + "unhandled control-flow operation: {}", + name + ))) + } + } else { + Ok(inst1.py_op_eq(py, inst2)? + && check_args() + && check_conditions()?) + } + } + [OperationRef::Gate(_op1), OperationRef::Gate(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + [OperationRef::Operation(_op1), OperationRef::Operation(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args()) + } + // Handle the case we end up with a pygate for a standardgate + // this typically only happens if it's a ControlledGate in python + // and we have mutable state set. + [OperationRef::Standard(_op1), OperationRef::Gate(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + [OperationRef::Gate(_op1), OperationRef::Standard(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + _ => Ok(false), + } + } + [NodeType::QubitIn(bit1), NodeType::QubitIn(bit2)] => Ok(bit1 == bit2), + [NodeType::ClbitIn(bit1), NodeType::ClbitIn(bit2)] => Ok(bit1 == bit2), + [NodeType::QubitOut(bit1), NodeType::QubitOut(bit2)] => Ok(bit1 == bit2), + [NodeType::ClbitOut(bit1), NodeType::ClbitOut(bit2)] => Ok(bit1 == bit2), + [NodeType::VarIn(var1), NodeType::VarIn(var2)] => var1.bind(py).eq(var2), + [NodeType::VarOut(var1), NodeType::VarOut(var2)] => var1.bind(py).eq(var2), + _ => Ok(false), + } + }; + + isomorphism::vf2::is_isomorphic( + &self.dag, + &other.dag, + node_match, + isomorphism::vf2::NoSemanticMatch, + true, + Ordering::Equal, + true, + None, + ) + .map_err(|e| match e { + isomorphism::vf2::IsIsomorphicError::NodeMatcherErr(e) => e, + _ => { + unreachable!() + } + }) + } + + /// Yield nodes in topological order. + /// + /// Args: + /// key (Callable): A callable which will take a DAGNode object and + /// return a string sort key. If not specified the + /// :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be + /// used as the sort key for each node. + /// + /// Returns: + /// generator(DAGOpNode, DAGInNode, or DAGOutNode): node in topological order + #[pyo3(name = "topological_nodes")] + fn py_topological_nodes( + &self, + py: Python, + key: Option>, + ) -> PyResult> { + let nodes: PyResult> = if let Some(key) = key { + self.topological_key_sort(py, &key)? + .map(|node| self.get_node(py, node)) + .collect() + } else { + // Good path, using interner IDs. + self.topological_nodes()? + .map(|n| self.get_node(py, n)) + .collect() + }; + + Ok(PyTuple::new_bound(py, nodes?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Yield op nodes in topological order. + /// + /// Allowed to pass in specific key to break ties in top order + /// + /// Args: + /// key (Callable): A callable which will take a DAGNode object and + /// return a string sort key. If not specified the + /// :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be + /// used as the sort key for each node. + /// + /// Returns: + /// generator(DAGOpNode): op node in topological order + #[pyo3(name = "topological_op_nodes")] + fn py_topological_op_nodes( + &self, + py: Python, + key: Option>, + ) -> PyResult> { + let nodes: PyResult> = if let Some(key) = key { + self.topological_key_sort(py, &key)? + .filter_map(|node| match self.dag.node_weight(node) { + Some(NodeType::Operation(_)) => Some(self.get_node(py, node)), + _ => None, + }) + .collect() + } else { + // Good path, using interner IDs. + self.topological_op_nodes()? + .map(|n| self.get_node(py, n)) + .collect() + }; + + Ok(PyTuple::new_bound(py, nodes?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Replace a block of nodes with a single node. + /// + /// This is used to consolidate a block of DAGOpNodes into a single + /// operation. A typical example is a block of gates being consolidated + /// into a single ``UnitaryGate`` representing the unitary matrix of the + /// block. + /// + /// Args: + /// node_block (List[DAGNode]): A list of dag nodes that represents the + /// node block to be replaced + /// op (qiskit.circuit.Operation): The operation to replace the + /// block with + /// wire_pos_map (Dict[Bit, int]): The dictionary mapping the bits to their positions in the + /// output ``qargs`` or ``cargs``. This is necessary to reconstruct the arg order over + /// multiple gates in the combined single op node. If a :class:`.Bit` is not in the + /// dictionary, it will not be added to the args; this can be useful when dealing with + /// control-flow operations that have inherent bits in their ``condition`` or ``target`` + /// fields. + /// cycle_check (bool): When set to True this method will check that + /// replacing the provided ``node_block`` with a single node + /// would introduce a cycle (which would invalidate the + /// ``DAGCircuit``) and will raise a ``DAGCircuitError`` if a cycle + /// would be introduced. This checking comes with a run time + /// penalty. If you can guarantee that your input ``node_block`` is + /// a contiguous block and won't introduce a cycle when it's + /// contracted to a single node, this can be set to ``False`` to + /// improve the runtime performance of this method. + /// + /// Raises: + /// DAGCircuitError: if ``cycle_check`` is set to ``True`` and replacing + /// the specified block introduces a cycle or if ``node_block`` is + /// empty. + /// + /// Returns: + /// DAGOpNode: The op node that replaces the block. + #[pyo3(signature = (node_block, op, wire_pos_map, cycle_check=true))] + fn replace_block_with_op( + &mut self, + py: Python, + node_block: Vec>, + op: Bound, + wire_pos_map: &Bound, + cycle_check: bool, + ) -> PyResult> { + // If node block is empty return early + if node_block.is_empty() { + return Err(DAGCircuitError::new_err( + "Can't replace an empty 'node_block'", + )); + } + + let mut qubit_pos_map: HashMap = HashMap::new(); + let mut clbit_pos_map: HashMap = HashMap::new(); + for (bit, index) in wire_pos_map.iter() { + if bit.is_instance(imports::QUBIT.get_bound(py))? { + qubit_pos_map.insert(self.qubits.find(&bit).unwrap(), index.extract()?); + } else if bit.is_instance(imports::CLBIT.get_bound(py))? { + clbit_pos_map.insert(self.clbits.find(&bit).unwrap(), index.extract()?); + } else { + return Err(DAGCircuitError::new_err( + "Wire map keys must be Qubit or Clbit instances.", + )); + } + } + + let block_ids: Vec<_> = node_block.iter().map(|n| n.node.unwrap()).collect(); + + let mut block_op_names = Vec::new(); + let mut block_qargs: HashSet = HashSet::new(); + let mut block_cargs: HashSet = HashSet::new(); + for nd in &block_ids { + let weight = self.dag.node_weight(*nd); + match weight { + Some(NodeType::Operation(packed)) => { + block_op_names.push(packed.op.name().to_string()); + block_qargs.extend(self.qargs_cache.intern(packed.qubits)); + block_cargs.extend(self.cargs_cache.intern(packed.clbits)); + + if let Some(condition) = packed.condition() { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .condition_resources(condition.bind(py))? + .clbits + .bind(py), + )?, + ); + continue; + } + + // Add classical bits from SwitchCaseOp, if applicable. + if let OperationRef::Instruction(op) = packed.op.view() { + if op.name() == "switch_case" { + let op_bound = op.instruction.bind(py); + let target = op_bound.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + block_cargs.insert(self.clbits.find(&target).unwrap()); + } else if target + .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? + { + block_cargs.extend( + self.clbits + .map_bits(target.extract::>>()?)?, + ); + } else { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .node_resources(&target)? + .clbits + .bind(py), + )?, + ); + } + } + } + } + Some(_) => { + return Err(DAGCircuitError::new_err( + "Nodes in 'node_block' must be of type 'DAGOpNode'.", + )) + } + None => { + return Err(DAGCircuitError::new_err( + "Node in 'node_block' not found in DAG.", + )) + } + } + } + + let mut block_qargs: Vec = block_qargs + .into_iter() + .filter(|q| qubit_pos_map.contains_key(q)) + .collect(); + block_qargs.sort_by_key(|q| qubit_pos_map[q]); + + let mut block_cargs: Vec = block_cargs + .into_iter() + .filter(|c| clbit_pos_map.contains_key(c)) + .collect(); + block_cargs.sort_by_key(|c| clbit_pos_map[c]); + + let py_op = op.extract::()?; + + if py_op.operation.num_qubits() as usize != block_qargs.len() { + return Err(DAGCircuitError::new_err(format!( + "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() + ))); + } + + let op_name = py_op.operation.name().to_string(); + let qubits = Interner::intern(&mut self.qargs_cache, block_qargs)?; + let clbits = Interner::intern(&mut self.cargs_cache, block_cargs)?; + let weight = NodeType::Operation(PackedInstruction { + op: py_op.operation, + qubits, + clbits, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }); + + let new_node = self + .dag + .contract_nodes(block_ids, weight, cycle_check) + .map_err(|e| match e { + ContractError::DAGWouldCycle => DAGCircuitError::new_err( + "Replacing the specified node block would introduce a cycle", + ), + })?; + + self.increment_op(op_name.as_str()); + for name in block_op_names { + self.decrement_op(name.as_str()); + } + + self.get_node(py, new_node) + } + + /// Replace one node with dag. + /// + /// Args: + /// node (DAGOpNode): node to substitute + /// input_dag (DAGCircuit): circuit that will substitute the node + /// wires (list[Bit] | Dict[Bit, Bit]): gives an order for (qu)bits + /// in the input circuit. If a list, then the bits refer to those in the ``input_dag``, + /// and the order gets matched to the node wires by qargs first, then cargs, then + /// conditions. If a dictionary, then a mapping of bits in the ``input_dag`` to those + /// that the ``node`` acts on. + /// propagate_condition (bool): If ``True`` (default), then any ``condition`` attribute on + /// the operation within ``node`` is propagated to each node in the ``input_dag``. If + /// ``False``, then the ``input_dag`` is assumed to faithfully implement suitable + /// conditional logic already. This is ignored for :class:`.ControlFlowOp`\\ s (i.e. + /// treated as if it is ``False``); replacements of those must already fulfill the same + /// conditional logic or this function would be close to useless for them. + /// + /// Returns: + /// dict: maps node IDs from `input_dag` to their new node incarnations in `self`. + /// + /// Raises: + /// DAGCircuitError: if met with unexpected predecessor/successors + #[pyo3(signature = (node, input_dag, wires=None, propagate_condition=true))] + fn substitute_node_with_dag( + &mut self, + py: Python, + node: &Bound, + input_dag: &DAGCircuit, + wires: Option>, + propagate_condition: bool, + ) -> PyResult> { + let (node_index, bound_node) = match node.downcast::() { + Ok(bound_node) => (bound_node.borrow().as_ref().node.unwrap(), bound_node), + Err(_) => return Err(DAGCircuitError::new_err("expected node DAGOpNode")), + }; + + let node = match &self.dag[node_index] { + NodeType::Operation(op) => op.clone(), + _ => return Err(DAGCircuitError::new_err("expected node")), + }; + + type WireMapsTuple = (HashMap, HashMap, Py); + + let build_wire_map = |wires: &Bound| -> PyResult { + let qargs_list = imports::BUILTIN_LIST + .get_bound(py) + .call1((bound_node.borrow().get_qargs(py),))?; + let qargs_list = qargs_list.downcast::().unwrap(); + let cargs_list = imports::BUILTIN_LIST + .get_bound(py) + .call1((bound_node.borrow().get_cargs(py),))?; + let cargs_list = cargs_list.downcast::().unwrap(); + let cargs_set = imports::BUILTIN_SET.get_bound(py).call1((cargs_list,))?; + let cargs_set = cargs_set.downcast::().unwrap(); + if !propagate_condition && self.may_have_additional_wires(py, &node) { + let (add_cargs, _add_vars) = + self.additional_wires(py, node.op.view(), node.condition())?; + for wire in add_cargs.iter() { + let clbit = &self.clbits.get(*wire).unwrap(); + if !cargs_set.contains(clbit.clone_ref(py))? { + cargs_list.append(clbit)?; + } + } + } + let qargs_len = qargs_list.len(); + let cargs_len = cargs_list.len(); + + if qargs_len + cargs_len != wires.len() { + return Err(DAGCircuitError::new_err(format!( + "bit mapping invalid: expected {}, got {}", + qargs_len + cargs_len, + wires.len() + ))); + } + let mut qubit_wire_map = HashMap::new(); + let mut clbit_wire_map = HashMap::new(); + let var_map = PyDict::new_bound(py); + for (index, wire) in wires.iter().enumerate() { + if wire.is_instance(imports::QUBIT.get_bound(py))? { + if index >= qargs_len { + unreachable!() + } + let input_qubit: Qubit = input_dag.qubits.find(&wire).unwrap(); + let self_qubit: Qubit = self.qubits.find(&qargs_list.get_item(index)?).unwrap(); + qubit_wire_map.insert(input_qubit, self_qubit); + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + if index < qargs_len { + unreachable!() + } + clbit_wire_map.insert( + input_dag.clbits.find(&wire).unwrap(), + self.clbits + .find(&cargs_list.get_item(index - qargs_len)?) + .unwrap(), + ); + } else { + return Err(DAGCircuitError::new_err( + "`Var` nodes cannot be remapped during substitution", + )); + } + } + Ok((qubit_wire_map, clbit_wire_map, var_map.unbind())) + }; + + let (mut qubit_wire_map, mut clbit_wire_map, var_map): ( + HashMap, + HashMap, + Py, + ) = match wires { + Some(wires) => match wires.downcast::() { + Ok(bound_wires) => { + let mut qubit_wire_map = HashMap::new(); + let mut clbit_wire_map = HashMap::new(); + let var_map = PyDict::new_bound(py); + for (source_wire, target_wire) in bound_wires.iter() { + if source_wire.is_instance(imports::QUBIT.get_bound(py))? { + qubit_wire_map.insert( + input_dag.qubits.find(&source_wire).unwrap(), + self.qubits.find(&target_wire).unwrap(), + ); + } else if source_wire.is_instance(imports::CLBIT.get_bound(py))? { + clbit_wire_map.insert( + input_dag.clbits.find(&source_wire).unwrap(), + self.clbits.find(&target_wire).unwrap(), + ); + } else { + var_map.set_item(source_wire, target_wire)?; + } + } + (qubit_wire_map, clbit_wire_map, var_map.unbind()) + } + Err(_) => { + let wires: Bound = match wires.downcast::() { + Ok(bound_list) => bound_list.clone(), + // If someone passes a sequence instead of an exact list (tuple is + // occasionally used) cast that to a list and then use it. + Err(_) => { + let raw_wires = imports::BUILTIN_LIST.get_bound(py).call1((wires,))?; + raw_wires.extract()? + } + }; + build_wire_map(&wires)? + } + }, + None => { + let raw_wires = input_dag.get_wires(py); + let binding = raw_wires?; + let wires = binding.bind(py); + build_wire_map(wires)? + } + }; + + let var_iter = input_dag.iter_vars(py)?; + let raw_set = imports::BUILTIN_SET.get_bound(py).call1((var_iter,))?; + let input_dag_var_set: &Bound = raw_set.downcast()?; + + let node_vars = if self.may_have_additional_wires(py, &node) { + let (_additional_clbits, additional_vars) = + self.additional_wires(py, node.op.view(), node.condition())?; + let var_set = PySet::new_bound(py, &additional_vars)?; + if input_dag_var_set + .call_method1(intern!(py, "difference"), (var_set.clone(),))? + .is_truthy()? + { + return Err(DAGCircuitError::new_err(format!( + "Cannot replace a node with a DAG with more variables. Variables in node: {:?}. Variables in dag: {:?}", + var_set.str(), input_dag_var_set.str(), + ))); + } + var_set + } else { + PySet::empty_bound(py)? + }; + let bound_var_map = var_map.bind(py); + for var in input_dag_var_set.iter() { + bound_var_map.set_item(var.clone(), var)?; + } + + for contracted_var in node_vars + .call_method1(intern!(py, "difference"), (input_dag_var_set,))? + .downcast::()? + .iter() + { + let pred = self + .dag + .edges_directed(node_index, Incoming) + .find(|edge| { + if let Wire::Var(var) = edge.weight() { + contracted_var.eq(var).unwrap() + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node_index, Outgoing) + .find(|edge| { + if let Wire::Var(var) = edge.weight() { + contracted_var.eq(var).unwrap() + } else { + false + } + }) + .unwrap(); + self.dag.add_edge( + pred.source(), + succ.target(), + Wire::Var(contracted_var.unbind()), + ); + } + + let mut new_input_dag: Option = None; + // It doesn't make sense to try and propagate a condition from a control-flow op; a + // replacement for the control-flow op should implement the operation completely. + let node_map = if propagate_condition && !node.op.control_flow() { + // Nested until https://github.com/rust-lang/rust/issues/53667 is fixed in a stable + // release + if let Some(condition) = node + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + let mut in_dag = input_dag.copy_empty_like(py, "alike")?; + // The remapping of `condition` below is still using the old code that assumes a 2-tuple. + // This is because this remapping code only makes sense in the case of non-control-flow + // operations being replaced. These can only have the 2-tuple conditions, and the + // ability to set a condition at an individual node level will be deprecated and removed + // in favour of the new-style conditional blocks. The extra logic in here to add + // additional wires into the map as necessary would hugely complicate matters if we tried + // to abstract it out into the `VariableMapper` used elsewhere. + let wire_map = PyDict::new_bound(py); + for (source_qubit, target_qubit) in &qubit_wire_map { + wire_map.set_item( + in_dag.qubits.get(*source_qubit).unwrap().clone_ref(py), + self.qubits.get(*target_qubit).unwrap().clone_ref(py), + )? + } + for (source_clbit, target_clbit) in &clbit_wire_map { + wire_map.set_item( + in_dag.clbits.get(*source_clbit).unwrap().clone_ref(py), + self.clbits.get(*target_clbit).unwrap().clone_ref(py), + )? + } + wire_map.update(var_map.bind(py).as_mapping())?; + + let reverse_wire_map = wire_map.iter().map(|(k, v)| (v, k)).into_py_dict_bound(py); + let (py_target, py_value): (Bound, Bound) = + condition.bind(py).extract()?; + let (py_new_target, target_cargs) = + if py_target.is_instance(imports::CLBIT.get_bound(py))? { + let new_target = reverse_wire_map + .get_item(&py_target)? + .map(Ok::<_, PyErr>) + .unwrap_or_else(|| { + // Target was not in node's wires, so we need a dummy. + let new_target = imports::CLBIT.get_bound(py).call0()?; + in_dag.add_clbit_unchecked(py, &new_target)?; + wire_map.set_item(&new_target, &py_target)?; + reverse_wire_map.set_item(&py_target, &new_target)?; + Ok(new_target) + })?; + (new_target.clone(), PySet::new_bound(py, &[new_target])?) + } else { + // ClassicalRegister + let target_bits: Vec> = + py_target.iter()?.collect::>()?; + let mapped_bits: Vec>> = target_bits + .iter() + .map(|b| reverse_wire_map.get_item(b)) + .collect::>()?; + + let mut new_target = Vec::with_capacity(target_bits.len()); + let target_cargs = PySet::empty_bound(py)?; + for (ours, theirs) in target_bits.into_iter().zip(mapped_bits) { + if let Some(theirs) = theirs { + // Target bit was in node's wires. + new_target.push(theirs.clone()); + target_cargs.add(theirs)?; + } else { + // Target bit was not in node's wires, so we need a dummy. + let theirs = imports::CLBIT.get_bound(py).call0()?; + in_dag.add_clbit_unchecked(py, &theirs)?; + wire_map.set_item(&theirs, &ours)?; + reverse_wire_map.set_item(&ours, &theirs)?; + new_target.push(theirs.clone()); + target_cargs.add(theirs)?; + } + } + let kwargs = [("bits", new_target.into_py(py))].into_py_dict_bound(py); + let new_target_register = imports::CLASSICAL_REGISTER + .get_bound(py) + .call((), Some(&kwargs))?; + in_dag.add_creg(py, &new_target_register)?; + (new_target_register, target_cargs) + }; + let new_condition = PyTuple::new_bound(py, [py_new_target, py_value]); + + qubit_wire_map.clear(); + clbit_wire_map.clear(); + for item in wire_map.items().iter() { + let (in_bit, self_bit): (Bound, Bound) = item.extract()?; + if in_bit.is_instance(imports::QUBIT.get_bound(py))? { + let in_index = in_dag.qubits.find(&in_bit).unwrap(); + let self_index = self.qubits.find(&self_bit).unwrap(); + qubit_wire_map.insert(in_index, self_index); + } else { + let in_index = in_dag.clbits.find(&in_bit).unwrap(); + let self_index = self.clbits.find(&self_bit).unwrap(); + clbit_wire_map.insert(in_index, self_index); + } + } + for in_node_index in input_dag.topological_op_nodes()? { + let in_node = &input_dag.dag[in_node_index]; + if let NodeType::Operation(inst) = in_node { + if inst + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + .is_some() + { + return Err(DAGCircuitError::new_err( + "cannot propagate a condition to an element that already has one", + )); + } + let cargs = input_dag.cargs_cache.intern(inst.clbits); + let cargs_bits: Vec = input_dag + .clbits + .map_indices(cargs) + .map(|x| x.clone_ref(py)) + .collect(); + if !target_cargs + .call_method1(intern!(py, "intersection"), (cargs_bits,))? + .downcast::()? + .is_empty() + { + return Err(DAGCircuitError::new_err("cannot propagate a condition to an element that acts on those bits")); + } + let mut new_inst = inst.clone(); + if new_condition.is_truthy()? { + if let Some(ref mut attrs) = new_inst.extra_attrs { + attrs.condition = Some(new_condition.as_any().clone().unbind()); + } else { + new_inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + condition: Some(new_condition.as_any().clone().unbind()), + label: None, + duration: None, + unit: None, + })); + } + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op.take(); + } + } + in_dag.push_back(py, new_inst)?; + } + } + let node_map = self.substitute_node_with_subgraph( + py, + node_index, + &in_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )?; + new_input_dag = Some(in_dag); + node_map + } else { + self.substitute_node_with_subgraph( + py, + node_index, + input_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )? + } + } else { + self.substitute_node_with_subgraph( + py, + node_index, + input_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )? + }; + self.global_phase = add_global_phase(py, &self.global_phase, &input_dag.global_phase)?; + + let wire_map_dict = PyDict::new_bound(py); + for (source, target) in clbit_wire_map.iter() { + let source_bit = match new_input_dag { + Some(ref in_dag) => in_dag.clbits.get(*source), + None => input_dag.clbits.get(*source), + }; + let target_bit = self.clbits.get(*target); + wire_map_dict.set_item(source_bit, target_bit)?; + } + let bound_var_map = var_map.bind(py); + + // Note: creating this list to hold new registers created by the mapper is a temporary + // measure until qiskit.expr is ported to Rust. It is necessary because we cannot easily + // have Python call back to DAGCircuit::add_creg while we're currently borrowing + // the DAGCircuit. + let new_registers = PyList::empty_bound(py); + let add_new_register = new_registers.getattr("append")?.unbind(); + let flush_new_registers = |dag: &mut DAGCircuit| -> PyResult<()> { + for reg in &new_registers { + dag.add_creg(py, ®)?; + } + new_registers.del_slice(0, new_registers.len())?; + Ok(()) + }; + + let variable_mapper = PyVariableMapper::new( + py, + self.cregs.bind(py).values().into_any(), + Some(wire_map_dict), + Some(bound_var_map.clone()), + Some(add_new_register), + )?; + + for (old_node_index, new_node_index) in node_map.iter() { + let old_node = match new_input_dag { + Some(ref in_dag) => &in_dag.dag[*old_node_index], + None => &input_dag.dag[*old_node_index], + }; + if let NodeType::Operation(old_inst) = old_node { + if let OperationRef::Instruction(old_op) = old_inst.op.view() { + if old_op.name() == "switch_case" { + let raw_target = old_op.instruction.getattr(py, "target")?; + let target = raw_target.bind(py); + let kwargs = PyDict::new_bound(py); + kwargs.set_item( + "label", + old_inst + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.label.as_ref()), + )?; + let new_op = imports::SWITCH_CASE_OP.get_bound(py).call( + ( + variable_mapper.map_target(target)?, + old_op.instruction.call_method0(py, "cases_specifier")?, + ), + Some(&kwargs), + )?; + flush_new_registers(self)?; + + if let NodeType::Operation(ref mut new_inst) = + &mut self.dag[*new_node_index] + { + new_inst.op = PyInstruction { + qubits: old_op.num_qubits(), + clbits: old_op.num_clbits(), + params: old_op.num_params(), + control_flow: old_op.control_flow(), + op_name: old_op.name().to_string(), + instruction: new_op.clone().unbind(), + } + .into(); + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op = new_op.unbind().into(); + } + } + } + } + if let Some(condition) = old_inst + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + { + if old_inst.op.name() != "switch_case" { + let new_condition: Option = variable_mapper + .map_condition(condition.bind(py), false)? + .extract()?; + flush_new_registers(self)?; + + if let NodeType::Operation(ref mut new_inst) = + &mut self.dag[*new_node_index] + { + match &mut new_inst.extra_attrs { + Some(attrs) => attrs.condition.clone_from(&new_condition), + None => { + new_inst.extra_attrs = + Some(Box::new(ExtraInstructionAttributes { + label: None, + condition: new_condition.clone(), + unit: None, + duration: None, + })) + } + } + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op.take(); + } + match new_inst.op.view() { + OperationRef::Instruction(py_inst) => { + py_inst + .instruction + .setattr(py, "condition", new_condition)?; + } + OperationRef::Gate(py_gate) => { + py_gate.gate.setattr(py, "condition", new_condition)?; + } + OperationRef::Operation(py_op) => { + py_op.operation.setattr(py, "condition", new_condition)?; + } + OperationRef::Standard(_) => {} + } + } + } + } + } + } + let out_dict = PyDict::new_bound(py); + for (old_index, new_index) in node_map { + out_dict.set_item(old_index.index(), self.get_node(py, new_index)?)?; + } + Ok(out_dict.unbind()) + } + + /// Replace a DAGOpNode with a single operation. qargs, cargs and + /// conditions for the new operation will be inferred from the node to be + /// replaced. The new operation will be checked to match the shape of the + /// replaced operation. + /// + /// Args: + /// node (DAGOpNode): Node to be replaced + /// op (qiskit.circuit.Operation): The :class:`qiskit.circuit.Operation` + /// instance to be added to the DAG + /// inplace (bool): Optional, default False. If True, existing DAG node + /// will be modified to include op. Otherwise, a new DAG node will + /// be used. + /// propagate_condition (bool): Optional, default True. If True, a condition on the + /// ``node`` to be replaced will be applied to the new ``op``. This is the legacy + /// behaviour. If either node is a control-flow operation, this will be ignored. If + /// the ``op`` already has a condition, :exc:`.DAGCircuitError` is raised. + /// + /// Returns: + /// DAGOpNode: the new node containing the added operation. + /// + /// Raises: + /// DAGCircuitError: If replacement operation was incompatible with + /// location of target node. + #[pyo3(signature = (node, op, inplace=false, propagate_condition=true))] + fn substitute_node( + &mut self, + node: &Bound, + op: &Bound, + inplace: bool, + propagate_condition: bool, + ) -> PyResult> { + let mut node: PyRefMut = match node.downcast() { + Ok(node) => node.borrow_mut(), + Err(_) => return Err(DAGCircuitError::new_err("Only DAGOpNodes can be replaced.")), + }; + let py = op.py(); + let node_index = node.as_ref().node.unwrap(); + // Extract information from node that is going to be replaced + let old_packed = match self.dag.node_weight(node_index) { + Some(NodeType::Operation(old_packed)) => old_packed.clone(), + Some(_) => { + return Err(DAGCircuitError::new_err( + "'node' must be of type 'DAGOpNode'.", + )) + } + None => return Err(DAGCircuitError::new_err("'node' not found in DAG.")), + }; + // Extract information from new op + let new_op = op.extract::()?; + let current_wires: HashSet = self + .dag + .edges(node_index) + .map(|e| e.weight().clone()) + .collect(); + let mut new_wires: HashSet = self + .qargs_cache + .intern(old_packed.qubits) + .iter() + .map(|x| Wire::Qubit(*x)) + .chain( + self.cargs_cache + .intern(old_packed.clbits) + .iter() + .map(|x| Wire::Clbit(*x)), + ) + .collect(); + let (additional_clbits, additional_vars) = self.additional_wires( + py, + new_op.operation.view(), + new_op + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()), + )?; + new_wires.extend(additional_clbits.iter().map(|x| Wire::Clbit(*x))); + new_wires.extend(additional_vars.iter().map(|x| Wire::Var(x.clone_ref(py)))); + + if old_packed.op.num_qubits() != new_op.operation.num_qubits() + || old_packed.op.num_clbits() != new_op.operation.num_clbits() + { + return Err(DAGCircuitError::new_err( + format!( + "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", + old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.operation.num_qubits(), new_op.operation.num_clbits() + ))); + } + + #[cfg(feature = "cache_pygates")] + let mut py_op_cache = Some(op.clone().unbind()); + + let mut extra_attrs = new_op.extra_attrs.clone(); + // If either operation is a control-flow operation, propagate_condition is ignored + if propagate_condition + && !(node.instruction.operation.control_flow() || new_op.operation.control_flow()) + { + // if new_op has a condition, the condition can't be propagated from the old node + if new_op + .extra_attrs + .as_ref() + .and_then(|extra| extra.condition.as_ref()) + .is_some() + { + return Err(DAGCircuitError::new_err( + "Cannot propagate a condition to an operation that already has one.", + )); + } + if let Some(old_condition) = old_packed.condition() { + if matches!(new_op.operation.view(), OperationRef::Operation(_)) { + return Err(DAGCircuitError::new_err( + "Cannot add a condition on a generic Operation.", + )); + } + if let Some(ref mut extra) = extra_attrs { + extra.condition = Some(old_condition.clone_ref(py)); + } else { + extra_attrs = ExtraInstructionAttributes::new( + None, + None, + None, + Some(old_condition.clone_ref(py)), + ) + .map(Box::new) + } + let binding = self + .control_flow_module + .condition_resources(old_condition.bind(py))?; + let condition_clbits = binding.clbits.bind(py); + for bit in condition_clbits { + new_wires.insert(Wire::Clbit(self.clbits.find(&bit).unwrap())); + } + let op_ref = new_op.operation.view(); + if let OperationRef::Instruction(inst) = op_ref { + inst.instruction + .bind(py) + .setattr(intern!(py, "condition"), old_condition)?; + } else if let OperationRef::Gate(gate) = op_ref { + gate.gate.bind(py).call_method1( + intern!(py, "c_if"), + old_condition.downcast_bound::(py)?, + )?; + } + #[cfg(feature = "cache_pygates")] + { + py_op_cache = None; + } + } + }; + if new_wires != current_wires { + // The new wires must be a non-strict subset of the current wires; if they add new + // wires, we'd not know where to cut the existing wire to insert the new dependency. + return Err(DAGCircuitError::new_err(format!( + "New operation '{:?}' does not span the same wires as the old node '{:?}'. New wires: {:?}, old_wires: {:?}.", op.str(), old_packed.op.view(), new_wires, current_wires + ))); + } + + if inplace { + node.instruction.operation = new_op.operation.clone(); + node.instruction.params = new_op.params.clone(); + node.instruction.extra_attrs = extra_attrs.clone(); + #[cfg(feature = "cache_pygates")] + { + node.instruction.py_op = py_op_cache + .as_ref() + .map(|ob| OnceCell::from(ob.clone_ref(py))) + .unwrap_or_default(); + } + } + // Clone op data, as it will be moved into the PackedInstruction + let new_weight = NodeType::Operation(PackedInstruction { + op: new_op.operation.clone(), + qubits: old_packed.qubits, + clbits: old_packed.clbits, + params: (!new_op.params.is_empty()).then(|| new_op.params.into()), + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: py_op_cache.map(OnceCell::from).unwrap_or_default(), + }); + let node_index = node.as_ref().node.unwrap(); + if let Some(weight) = self.dag.node_weight_mut(node_index) { + *weight = new_weight; + } + + // Update self.op_names + self.decrement_op(old_packed.op.name()); + self.increment_op(new_op.operation.name()); + + if inplace { + Ok(node.into_py(py)) + } else { + self.get_node(py, node_index) + } + } + + /// Decompose the circuit into sets of qubits with no gates connecting them. + /// + /// Args: + /// remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from + /// the separated circuits. If ``False``, each output circuit will contain the + /// same number of qubits as ``self``. + /// + /// Returns: + /// List[DAGCircuit]: The circuits resulting from separating ``self`` into sets + /// of disconnected qubits + /// + /// Each :class:`~.DAGCircuit` instance returned by this method will contain the same number of + /// clbits as ``self``. The global phase information in ``self`` will not be maintained + /// in the subcircuits returned by this method. + #[pyo3(signature = (remove_idle_qubits=false, *, vars_mode="alike"))] + fn separable_circuits( + &self, + py: Python, + remove_idle_qubits: bool, + vars_mode: &str, + ) -> PyResult> { + let connected_components = rustworkx_core::connectivity::connected_components(&self.dag); + let dags = PyList::empty_bound(py); + + for comp_nodes in connected_components.iter() { + let mut new_dag = self.copy_empty_like(py, vars_mode)?; + new_dag.global_phase = Param::Float(0.); + + // A map from nodes in the this DAGCircuit to nodes in the new dag. Used for adding edges + let mut node_map: HashMap = + HashMap::with_capacity(comp_nodes.len()); + + // Adding the nodes to the new dag + let mut non_classical = false; + for node in comp_nodes { + match self.dag.node_weight(*node) { + Some(w) => match w { + NodeType::ClbitIn(b) => { + let clbit_in = new_dag.clbit_io_map[b.0 as usize][0]; + node_map.insert(*node, clbit_in); + } + NodeType::ClbitOut(b) => { + let clbit_out = new_dag.clbit_io_map[b.0 as usize][1]; + node_map.insert(*node, clbit_out); + } + NodeType::QubitIn(q) => { + let qbit_in = new_dag.qubit_io_map[q.0 as usize][0]; + node_map.insert(*node, qbit_in); + non_classical = true; + } + NodeType::QubitOut(q) => { + let qbit_out = new_dag.qubit_io_map[q.0 as usize][1]; + node_map.insert(*node, qbit_out); + non_classical = true; + } + NodeType::VarIn(v) => { + let var_in = new_dag.var_input_map.get(py, v).unwrap(); + node_map.insert(*node, var_in); + } + NodeType::VarOut(v) => { + let var_out = new_dag.var_output_map.get(py, v).unwrap(); + node_map.insert(*node, var_out); + } + NodeType::Operation(pi) => { + let new_node = new_dag.dag.add_node(NodeType::Operation(pi.clone())); + new_dag.increment_op(pi.op.name()); + node_map.insert(*node, new_node); + non_classical = true; + } + }, + None => panic!("DAG node without payload!"), + } + } + if !non_classical { + continue; + } + let node_filter = |node: NodeIndex| -> bool { node_map.contains_key(&node) }; + + let filtered = NodeFiltered(&self.dag, node_filter); + + // Remove the edges added by copy_empty_like (as idle wires) to avoid duplication + new_dag.dag.clear_edges(); + for edge in filtered.edge_references() { + let new_source = node_map[&edge.source()]; + let new_target = node_map[&edge.target()]; + new_dag + .dag + .add_edge(new_source, new_target, edge.weight().clone()); + } + // Add back any edges for idle wires + for (qubit, [in_node, out_node]) in new_dag + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + if new_dag.dag.edges(*in_node).next().is_none() { + new_dag + .dag + .add_edge(*in_node, *out_node, Wire::Qubit(qubit)); + } + } + for (clbit, [in_node, out_node]) in new_dag + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + if new_dag.dag.edges(*in_node).next().is_none() { + new_dag + .dag + .add_edge(*in_node, *out_node, Wire::Clbit(clbit)); + } + } + for (var, in_node) in new_dag.var_input_map.iter(py) { + if new_dag.dag.edges(in_node).next().is_none() { + let out_node = new_dag.var_output_map.get(py, &var).unwrap(); + new_dag + .dag + .add_edge(in_node, out_node, Wire::Var(var.clone_ref(py))); + } + } + if remove_idle_qubits { + let idle_wires: Vec> = new_dag + .idle_wires(py, None)? + .into_bound(py) + .map(|q| q.unwrap()) + .filter(|e| e.is_instance(imports::QUBIT.get_bound(py)).unwrap()) + .collect(); + + let qubits = PyTuple::new_bound(py, idle_wires); + new_dag.remove_qubits(py, &qubits)?; // TODO: this does not really work, some issue with remove_qubits itself + } + + dags.append(pyo3::Py::new(py, new_dag)?)?; + } + + Ok(dags.unbind()) + } + + /// Swap connected nodes e.g. due to commutation. + /// + /// Args: + /// node1 (OpNode): predecessor node + /// node2 (OpNode): successor node + /// + /// Raises: + /// DAGCircuitError: if either node is not an OpNode or nodes are not connected + fn swap_nodes(&mut self, node1: &DAGNode, node2: &DAGNode) -> PyResult<()> { + let node1 = node1.node.unwrap(); + let node2 = node2.node.unwrap(); + + // Check that both nodes correspond to operations + if !matches!(self.dag.node_weight(node1).unwrap(), NodeType::Operation(_)) + || !matches!(self.dag.node_weight(node2).unwrap(), NodeType::Operation(_)) + { + return Err(DAGCircuitError::new_err( + "Nodes to swap are not both DAGOpNodes", + )); + } + + // Gather all wires connecting node1 and node2. + // This functionality was extracted from rustworkx's 'get_edge_data' + let wires: Vec = self + .dag + .edges(node1) + .filter(|edge| edge.target() == node2) + .map(|edge| edge.weight().clone()) + .collect(); + + if wires.is_empty() { + return Err(DAGCircuitError::new_err( + "Attempt to swap unconnected nodes", + )); + }; + + // Closure that finds the first parent/child node connected to a reference node by given wire + // and returns relevant edge information depending on the specified direction: + // - Incoming -> parent -> outputs (parent_edge_id, parent_source_node_id) + // - Outgoing -> child -> outputs (child_edge_id, child_target_node_id) + // This functionality was inspired in rustworkx's 'find_predecessors_by_edge' and 'find_successors_by_edge'. + let directed_edge_for_wire = |node: NodeIndex, direction: Direction, wire: &Wire| { + for edge in self.dag.edges_directed(node, direction) { + if wire == edge.weight() { + match direction { + Incoming => return Some((edge.id(), edge.source())), + Outgoing => return Some((edge.id(), edge.target())), + } + } + } + None + }; + + // Vector that contains a tuple of (wire, edge_info, parent_info, child_info) per wire in wires + let relevant_edges = wires + .iter() + .rev() + .map(|wire| { + ( + wire, + directed_edge_for_wire(node1, Outgoing, wire).unwrap(), + directed_edge_for_wire(node1, Incoming, wire).unwrap(), + directed_edge_for_wire(node2, Outgoing, wire).unwrap(), + ) + }) + .collect::>(); + + // Iterate over relevant edges and modify self.dag + for (wire, (node1_to_node2, _), (parent_to_node1, parent), (node2_to_child, child)) in + relevant_edges + { + self.dag.remove_edge(parent_to_node1); + self.dag.add_edge(parent, node2, wire.clone()); + self.dag.remove_edge(node1_to_node2); + self.dag.add_edge(node2, node1, wire.clone()); + self.dag.remove_edge(node2_to_child); + self.dag.add_edge(node1, child, wire.clone()); + } + Ok(()) + } + + /// Get the node in the dag. + /// + /// Args: + /// node_id(int): Node identifier. + /// + /// Returns: + /// node: the node. + fn node(&self, py: Python, node_id: isize) -> PyResult> { + self.get_node(py, NodeIndex::new(node_id as usize)) + } + + /// Iterator for node values. + /// + /// Yield: + /// node: the node. + fn nodes(&self, py: Python) -> PyResult> { + let result: PyResult> = self + .dag + .node_references() + .map(|(node, weight)| self.unpack_into(py, node, weight)) + .collect(); + let tup = PyTuple::new_bound(py, result?); + Ok(tup.into_any().iter().unwrap().unbind()) + } + + /// Iterator for edge values with source and destination node. + /// + /// This works by returning the outgoing edges from the specified nodes. If + /// no nodes are specified all edges from the graph are returned. + /// + /// Args: + /// nodes(DAGOpNode, DAGInNode, or DAGOutNode|list(DAGOpNode, DAGInNode, or DAGOutNode): + /// Either a list of nodes or a single input node. If none is specified, + /// all edges are returned from the graph. + /// + /// Yield: + /// edge: the edge as a tuple with the format + /// (source node, destination node, edge wire) + fn edges(&self, nodes: Option>, py: Python) -> PyResult> { + let get_node_index = |obj: &Bound| -> PyResult { + Ok(obj.downcast::()?.borrow().node.unwrap()) + }; + + let actual_nodes: Vec<_> = match nodes { + None => self.dag.node_indices().collect(), + Some(nodes) => { + let mut out = Vec::new(); + if let Ok(node) = get_node_index(&nodes) { + out.push(node); + } else { + for node in nodes.iter()? { + out.push(get_node_index(&node?)?); + } + } + out + } + }; + + let mut edges = Vec::new(); + for node in actual_nodes { + for edge in self.dag.edges_directed(node, Outgoing) { + edges.push(( + self.get_node(py, edge.source())?, + self.get_node(py, edge.target())?, + match edge.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + )) + } + } + + Ok(PyTuple::new_bound(py, edges) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Get the list of "op" nodes in the dag. + /// + /// Args: + /// op (Type): :class:`qiskit.circuit.Operation` subclass op nodes to + /// return. If None, return all op nodes. + /// include_directives (bool): include `barrier`, `snapshot` etc. + /// + /// Returns: + /// list[DAGOpNode]: the list of node ids containing the given op. + #[pyo3(name= "op_nodes", signature=(op=None, include_directives=true))] + fn py_op_nodes( + &self, + py: Python, + op: Option<&Bound>, + include_directives: bool, + ) -> PyResult>> { + let mut nodes = Vec::new(); + let filter_is_nonstandard = if let Some(op) = op { + op.getattr(intern!(py, "_standard_gate")).ok().is_none() + } else { + true + }; + for (node, weight) in self.dag.node_references() { + if let NodeType::Operation(packed) = &weight { + if !include_directives && packed.op.directive() { + continue; + } + if let Some(op_type) = op { + // This middle catch is to avoid Python-space operation creation for most uses of + // `op`; we're usually just looking for control-flow ops, and standard gates + // aren't control-flow ops. + if !(filter_is_nonstandard && packed.op.try_standard_gate().is_some()) + && packed.op.py_op_is_instance(op_type)? + { + nodes.push(self.unpack_into(py, node, weight)?); + } + } else { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Get the list of gate nodes in the dag. + /// + /// Returns: + /// list[DAGOpNode]: the list of DAGOpNodes that represent gates. + fn gate_nodes(&self, py: Python) -> PyResult>> { + self.dag + .node_references() + .filter_map(|(node, weight)| match weight { + NodeType::Operation(ref packed) => match packed.op.view() { + OperationRef::Gate(_) | OperationRef::Standard(_) => { + Some(self.unpack_into(py, node, weight)) + } + _ => None, + }, + _ => None, + }) + .collect() + } + + /// Get the set of "op" nodes with the given name. + #[pyo3(signature = (*names))] + fn named_nodes(&self, py: Python<'_>, names: &Bound) -> PyResult>> { + let mut names_set: HashSet = HashSet::with_capacity(names.len()); + for name_obj in names.iter() { + names_set.insert(name_obj.extract::()?); + } + let mut result: Vec> = Vec::new(); + for (id, weight) in self.dag.node_references() { + if let NodeType::Operation(ref packed) = weight { + if names_set.contains(packed.op.name()) { + result.push(self.unpack_into(py, id, weight)?); + } + } + } + Ok(result) + } + + /// Get list of 2 qubit operations. Ignore directives like snapshot and barrier. + fn 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_cache.intern(packed.qubits); + if qargs.len() == 2 { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Get list of 3+ qubit operations. Ignore directives like snapshot and barrier. + fn multi_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_cache.intern(packed.qubits); + if qargs.len() >= 3 { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Returns the longest path in the dag as a list of DAGOpNodes, DAGInNodes, and DAGOutNodes. + fn longest_path(&self, py: Python) -> PyResult> { + let weight_fn = |_| -> Result { Ok(1) }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.0, + None => return Err(DAGCircuitError::new_err("not a DAG")), + } + .into_iter() + .map(|node_index| self.get_node(py, node_index)) + .collect() + } + + /// Returns iterator of the successors of a node as DAGOpNodes and DAGOutNodes.""" + fn successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Outgoing) + .unique() + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, successors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes. + fn predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Incoming) + .unique() + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of "op" successors of a node in the dag. + fn op_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Outgoing) + .unique() + .filter_map(|i| match self.dag[i] { + NodeType::Operation(_) => Some(self.get_node(py, i)), + _ => None, + }) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns the iterator of "op" predecessors of a node in the dag. + fn op_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Incoming) + .unique() + .filter_map(|i| match self.dag[i] { + NodeType::Operation(_) => Some(self.get_node(py, i)), + _ => None, + }) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Checks if a second node is in the successors of node. + fn is_successor(&self, node: &DAGNode, node_succ: &DAGNode) -> bool { + self.dag + .find_edge(node.node.unwrap(), node_succ.node.unwrap()) + .is_some() + } + + /// Checks if a second node is in the predecessors of node. + fn is_predecessor(&self, node: &DAGNode, node_pred: &DAGNode) -> bool { + self.dag + .find_edge(node_pred.node.unwrap(), node.node.unwrap()) + .is_some() + } + + /// Returns iterator of the predecessors of a node that are + /// connected by a quantum edge as DAGOpNodes and DAGInNodes. + #[pyo3(name = "quantum_predecessors")] + fn py_quantum_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .quantum_predecessors(node.node.unwrap()) + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the successors of a node that are + /// connected by a quantum edge as DAGOpNodes and DAGOutNodes. + #[pyo3(name = "quantum_successors")] + fn py_quantum_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successors: PyResult> = self + .quantum_successors(node.node.unwrap()) + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, successors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the predecessors of a node that are + /// connected by a classical edge as DAGOpNodes and DAGInNodes. + fn classical_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let edges = self.dag.edges_directed(node.node.unwrap(), Incoming); + let filtered = edges.filter_map(|e| match e.weight() { + Wire::Qubit(_) => None, + _ => Some(e.source()), + }); + let predecessors: PyResult> = + filtered.unique().map(|i| self.get_node(py, i)).collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns set of the ancestors of a node as DAGOpNodes and DAGInNodes. + #[pyo3(name = "ancestors")] + fn py_ancestors(&self, py: Python, node: &DAGNode) -> PyResult> { + let ancestors: PyResult> = self + .ancestors(node.node.unwrap()) + .map(|node| self.get_node(py, node)) + .collect(); + Ok(PySet::new_bound(py, &ancestors?)?.unbind()) + } + + /// Returns set of the descendants of a node as DAGOpNodes and DAGOutNodes. + #[pyo3(name = "descendants")] + fn py_descendants(&self, py: Python, node: &DAGNode) -> PyResult> { + let descendants: PyResult> = self + .descendants(node.node.unwrap()) + .map(|node| self.get_node(py, node)) + .collect(); + Ok(PySet::new_bound(py, &descendants?)?.unbind()) + } + + /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node + /// and [DAGNode] is its successors in BFS order. + #[pyo3(name = "bfs_successors")] + fn py_bfs_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successor_index: PyResult)>> = self + .bfs_successors(node.node.unwrap()) + .map(|(node, nodes)| -> PyResult<(PyObject, Vec)> { + Ok(( + self.get_node(py, node)?, + nodes + .iter() + .map(|sub_node| self.get_node(py, *sub_node)) + .collect::>>()?, + )) + }) + .collect(); + Ok(PyList::new_bound(py, successor_index?) + .into_any() + .iter()? + .unbind()) + } + + /// Returns iterator of the successors of a node that are + /// connected by a classical edge as DAGOpNodes and DAGOutNodes. + fn classical_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let edges = self.dag.edges_directed(node.node.unwrap(), Outgoing); + let filtered = edges.filter_map(|e| match e.weight() { + Wire::Qubit(_) => None, + _ => Some(e.target()), + }); + let predecessors: PyResult> = + filtered.unique().map(|i| self.get_node(py, i)).collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Remove an operation node n. + /// + /// Add edges from predecessors to successors. + #[pyo3(name = "remove_op_node")] + fn py_remove_op_node(&mut self, node: &Bound) -> PyResult<()> { + let node: PyRef = match node.downcast::() { + Ok(node) => node.borrow(), + Err(_) => return Err(DAGCircuitError::new_err("Node not an DAGOpNode")), + }; + let index = node.as_ref().node.unwrap(); + if self.dag.node_weight(index).is_none() { + return Err(DAGCircuitError::new_err("Node not in DAG")); + } + self.remove_op_node(index); + Ok(()) + } + + /// Remove all of the ancestor operation nodes of node. + fn remove_ancestors_of(&mut self, node: &DAGNode) -> PyResult<()> { + let ancestors: Vec<_> = core_ancestors(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + for a in ancestors { + self.dag.remove_node(a); + } + Ok(()) + } + + /// Remove all of the descendant operation nodes of node. + fn remove_descendants_of(&mut self, node: &DAGNode) -> PyResult<()> { + let descendants: Vec<_> = core_descendants(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + for d in descendants { + self.dag.remove_node(d); + } + Ok(()) + } + + /// Remove all of the non-ancestors operation nodes of node. + fn remove_nonancestors_of(&mut self, node: &DAGNode) -> PyResult<()> { + let ancestors: HashSet<_> = core_ancestors(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + let non_ancestors: Vec<_> = self + .dag + .node_indices() + .filter(|node_id| !ancestors.contains(node_id)) + .collect(); + for na in non_ancestors { + self.dag.remove_node(na); + } + Ok(()) + } + + /// Remove all of the non-descendants operation nodes of node. + fn remove_nondescendants_of(&mut self, node: &DAGNode) -> PyResult<()> { + let descendants: HashSet<_> = core_descendants(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + let non_descendants: Vec<_> = self + .dag + .node_indices() + .filter(|node_id| !descendants.contains(node_id)) + .collect(); + for nd in non_descendants { + self.dag.remove_node(nd); + } + Ok(()) + } + + /// Return a list of op nodes in the first layer of this dag. + #[pyo3(name = "front_layer")] + fn py_front_layer(&self, py: Python) -> PyResult> { + let native_front_layer = self.front_layer(py); + let front_layer_list = PyList::empty_bound(py); + for node in native_front_layer { + front_layer_list.append(self.get_node(py, node)?)?; + } + Ok(front_layer_list.into()) + } + + /// Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit. + /// + /// A layer is a circuit whose gates act on disjoint qubits, i.e., + /// a layer has depth 1. The total number of layers equals the + /// circuit depth d. The layers are indexed from 0 to d-1 with the + /// earliest layer at index 0. The layers are constructed using a + /// greedy algorithm. Each returned layer is a dict containing + /// {"graph": circuit graph, "partition": list of qubit lists}. + /// + /// The returned layer contains new (but semantically equivalent) DAGOpNodes, DAGInNodes, + /// and DAGOutNodes. These are not the same as nodes of the original dag, but are equivalent + /// via DAGNode.semantic_eq(node1, node2). + /// + /// TODO: Gates that use the same cbits will end up in different + /// layers as this is currently implemented. This may not be + /// the desired behavior. + #[pyo3(signature = (*, vars_mode="captures"))] + fn layers(&self, py: Python, vars_mode: &str) -> PyResult> { + let layer_list = PyList::empty_bound(py); + let mut graph_layers = self.multigraph_layers(py); + if graph_layers.next().is_none() { + return Ok(PyIterator::from_bound_object(&layer_list)?.into()); + } + + for graph_layer in graph_layers { + let layer_dict = PyDict::new_bound(py); + // Sort to make sure they are in the order they were added to the original DAG + // It has to be done by node_id as graph_layer is just a list of nodes + // with no implied topology + // Drawing tools rely on _node_id to infer order of node creation + // so we need this to be preserved by layers() + // Get the op nodes from the layer, removing any input and output nodes. + let mut op_nodes: Vec<(&PackedInstruction, &NodeIndex)> = graph_layer + .iter() + .filter_map(|node| self.dag.node_weight(*node).map(|dag_node| (dag_node, node))) + .filter_map(|(node, index)| match node { + NodeType::Operation(oper) => Some((oper, index)), + _ => None, + }) + .collect(); + op_nodes.sort_by_key(|(_, node_index)| **node_index); + + if op_nodes.is_empty() { + return Ok(PyIterator::from_bound_object(&layer_list)?.into()); + } + + let mut new_layer = self.copy_empty_like(py, vars_mode)?; + + for (node, _) in op_nodes { + new_layer.push_back(py, node.clone())?; + } + + let new_layer_op_nodes = new_layer.op_nodes(false).filter_map(|node_index| { + match new_layer.dag.node_weight(node_index) { + Some(NodeType::Operation(ref node)) => Some(node), + _ => None, + } + }); + let support_iter = new_layer_op_nodes.into_iter().map(|node| { + PyTuple::new_bound( + py, + new_layer + .qubits + .map_indices(new_layer.qargs_cache.intern(node.qubits)), + ) + }); + let support_list = PyList::empty_bound(py); + for support_qarg in support_iter { + support_list.append(support_qarg)?; + } + layer_dict.set_item("graph", new_layer.into_py(py))?; + layer_dict.set_item("partition", support_list)?; + layer_list.append(layer_dict)?; + } + Ok(layer_list.into_any().iter()?.into()) + } + + /// Yield a layer for all gates of this circuit. + /// + /// A serial layer is a circuit with one gate. The layers have the + /// same structure as in layers(). + #[pyo3(signature = (*, vars_mode="captures"))] + fn serial_layers(&self, py: Python, vars_mode: &str) -> PyResult> { + let layer_list = PyList::empty_bound(py); + for next_node in self.topological_op_nodes()? { + let retrieved_node: &PackedInstruction = match self.dag.node_weight(next_node) { + Some(NodeType::Operation(node)) => node, + _ => unreachable!("A non-operation node was obtained from topological_op_nodes."), + }; + let mut new_layer = self.copy_empty_like(py, vars_mode)?; + + // Save the support of the operation we add to the layer + let support_list = PyList::empty_bound(py); + let qubits = PyTuple::new_bound( + py, + self.qargs_cache + .intern(retrieved_node.qubits) + .iter() + .map(|qubit| self.qubits.get(*qubit)), + ) + .unbind(); + new_layer.push_back(py, retrieved_node.clone())?; + + if !retrieved_node.op.directive() { + support_list.append(qubits)?; + } + + let layer_dict = [ + ("graph", new_layer.into_py(py)), + ("partition", support_list.into_any().unbind()), + ] + .into_py_dict_bound(py); + layer_list.append(layer_dict)?; + } + + Ok(layer_list.into_any().iter()?.into()) + } + + /// Yield layers of the multigraph. + #[pyo3(name = "multigraph_layers")] + fn py_multigraph_layers(&self, py: Python) -> PyResult> { + let graph_layers = self.multigraph_layers(py).map(|layer| -> Vec { + layer + .into_iter() + .filter_map(|index| self.get_node(py, index).ok()) + .collect() + }); + let list: Bound = + PyList::new_bound(py, graph_layers.collect::>>()); + Ok(PyIterator::from_bound_object(&list)?.unbind()) + } + + /// Return a set of non-conditional runs of "op" nodes with the given names. + /// + /// For example, "... h q[0]; cx q[0],q[1]; cx q[0],q[1]; h q[1]; .." + /// would produce the tuple of cx nodes as an element of the set returned + /// from a call to collect_runs(["cx"]). If instead the cx nodes were + /// "cx q[0],q[1]; cx q[1],q[0];", the method would still return the + /// pair in a tuple. The namelist can contain names that are not + /// in the circuit's basis. + /// + /// Nodes must have only one successor to continue the run. + #[pyo3(name = "collect_runs")] + fn py_collect_runs(&self, py: Python, namelist: &Bound) -> PyResult> { + let mut name_list_set = HashSet::with_capacity(namelist.len()); + for name in namelist.iter() { + name_list_set.insert(name.extract::()?); + } + match self.collect_runs(name_list_set) { + Some(runs) => { + let run_iter = runs.map(|node_indices| { + PyTuple::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_set = PySet::empty_bound(py)?; + for run_tuple in run_iter { + out_set.add(run_tuple)?; + } + Ok(out_set.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Return a set of non-conditional runs of 1q "op" nodes. + #[pyo3(name = "collect_1q_runs")] + fn py_collect_1q_runs(&self, py: Python) -> PyResult> { + match self.collect_1q_runs() { + Some(runs) => { + let runs_iter = runs.map(|node_indices| { + PyList::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_list = PyList::empty_bound(py); + for run_list in runs_iter { + out_list.append(run_list)?; + } + Ok(out_list.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Return a set of non-conditional runs of 2q "op" nodes. + #[pyo3(name = "collect_2q_runs")] + fn py_collect_2q_runs(&self, py: Python) -> PyResult> { + match self.collect_2q_runs() { + Some(runs) => { + let runs_iter = runs.into_iter().map(|node_indices| { + PyList::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_list = PyList::empty_bound(py); + for run_list in runs_iter { + out_list.append(run_list)?; + } + Ok(out_list.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Iterator for nodes that affect a given wire. + /// + /// Args: + /// wire (Bit): the wire to be looked at. + /// only_ops (bool): True if only the ops nodes are wanted; + /// otherwise, all nodes are returned. + /// Yield: + /// Iterator: the successive nodes on the given wire + /// + /// Raises: + /// DAGCircuitError: if the given wire doesn't exist in the DAG + #[pyo3(name = "nodes_on_wire", signature = (wire, only_ops=false))] + fn py_nodes_on_wire( + &self, + py: Python, + wire: &Bound, + only_ops: bool, + ) -> PyResult> { + let wire = if wire.is_instance(imports::QUBIT.get_bound(py))? { + self.qubits.find(wire).map(Wire::Qubit) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + self.clbits.find(wire).map(Wire::Clbit) + } else if self.var_input_map.contains_key(py, &wire.clone().unbind()) { + Some(Wire::Var(wire.clone().unbind())) + } else { + None + } + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given wire {:?} is not present in the circuit", + wire + )) + })?; + + let nodes = self + .nodes_on_wire(py, &wire, only_ops) + .into_iter() + .map(|n| self.get_node(py, n)) + .collect::>>()?; + Ok(PyTuple::new_bound(py, nodes).into_any().iter()?.unbind()) + } + + /// Count the occurrences of operation names. + /// + /// Args: + /// recurse: if ``True`` (default), then recurse into control-flow operations. In all + /// cases, this counts only the number of times the operation appears in any possible + /// block; both branches of if-elses are counted, and for- and while-loop blocks are + /// only counted once. + /// + /// 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 + || !CONTROL_FLOW_OP_NAMES + .iter() + .any(|x| self.op_names.contains_key(*x)) + { + 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.py_op_nodes( + py, + Some(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?), + true, + )? { + let raw_blocks = node.getattr(py, "op")?.getattr(py, "blocks")?; + let blocks: &Bound = raw_blocks.downcast_bound::(py)?; + for block in blocks.iter() { + let inner_dag: &DAGCircuit = + &circuit_to_dag.call1((block.clone(),))?.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)) + } + } + + /// Count the occurrences of operation names on the longest path. + /// + /// Returns a dictionary of counts keyed on the operation name. + fn count_ops_longest_path(&self) -> PyResult> { + if self.dag.node_count() == 0 { + return Ok(HashMap::new()); + } + let weight_fn = |_| -> Result { Ok(1) }; + let longest_path = + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.0, + None => return Err(DAGCircuitError::new_err("not a DAG")), + }; + // Allocate for worst case where all operations are unique + let mut op_counts: HashMap<&str, usize> = HashMap::with_capacity(longest_path.len() - 2); + for node_index in &longest_path[1..longest_path.len() - 1] { + if let NodeType::Operation(ref packed) = self.dag[*node_index] { + let name = packed.op.name(); + op_counts + .entry(name) + .and_modify(|count| *count += 1) + .or_insert(1); + } + } + Ok(op_counts) + } + + /// Returns causal cone of a qubit. + /// + /// A qubit's causal cone is the set of qubits that can influence the output of that + /// qubit through interactions, whether through multi-qubit gates or operations. Knowing + /// the causal cone of a qubit can be useful when debugging faulty circuits, as it can + /// help identify which wire(s) may be causing the problem. + /// + /// This method does not consider any classical data dependency in the ``DAGCircuit``, + /// classical bit wires are ignored for the purposes of building the causal cone. + /// + /// Args: + /// qubit (~qiskit.circuit.Qubit): The output qubit for which we want to find the causal cone. + /// + /// Returns: + /// Set[~qiskit.circuit.Qubit]: The set of qubits whose interactions affect ``qubit``. + fn quantum_causal_cone(&self, py: Python, qubit: &Bound) -> PyResult> { + // Retrieve the output node from the qubit + let output_qubit = self.qubits.find(qubit).ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given qubit {:?} is not present in the circuit", + qubit + )) + })?; + let output_node_index = self + .qubit_io_map + .get(output_qubit.0 as usize) + .map(|x| x[1]) + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given qubit {:?} is not present in qubit_output_map", + qubit + )) + })?; + + let mut qubits_in_cone: HashSet<&Qubit> = HashSet::from([&output_qubit]); + let mut queue: VecDeque = self.quantum_predecessors(output_node_index).collect(); + + // The processed_non_directive_nodes stores the set of processed non-directive nodes. + // This is an optimization to avoid considering the same non-directive node multiple + // times when reached from different paths. + // The directive nodes (such as barriers or measures) are trickier since when processing + // them we only add their predecessors that intersect qubits_in_cone. Hence, directive + // nodes have to be considered multiple times. + let mut processed_non_directive_nodes: HashSet = HashSet::new(); + + while !queue.is_empty() { + let cur_index = queue.pop_front().unwrap(); + + if let NodeType::Operation(packed) = self.dag.node_weight(cur_index).unwrap() { + if !packed.op.directive() { + // If the operation is not a directive (in particular not a barrier nor a measure), + // we do not do anything if it was already processed. Otherwise, we add its qubits + // to qubits_in_cone, and append its predecessors to queue. + if processed_non_directive_nodes.contains(&cur_index) { + continue; + } + qubits_in_cone.extend(self.qargs_cache.intern(packed.qubits).iter()); + processed_non_directive_nodes.insert(cur_index); + + for pred_index in self.quantum_predecessors(cur_index) { + if let NodeType::Operation(_pred_packed) = + self.dag.node_weight(pred_index).unwrap() + { + queue.push_back(pred_index); + } + } + } else { + // Directives (such as barriers and measures) may be defined over all the qubits, + // yet not all of these qubits should be considered in the causal cone. So we + // only add those predecessors that have qubits in common with qubits_in_cone. + for pred_index in self.quantum_predecessors(cur_index) { + if let NodeType::Operation(pred_packed) = + self.dag.node_weight(pred_index).unwrap() + { + if self + .qargs_cache + .intern(pred_packed.qubits) + .iter() + .any(|x| qubits_in_cone.contains(x)) + { + queue.push_back(pred_index); + } + } + } + } + } + } + + let qubits_in_cone_vec: Vec<_> = qubits_in_cone.iter().map(|&&qubit| qubit).collect(); + let elements = self.qubits.map_indices(&qubits_in_cone_vec[..]); + Ok(PySet::new_bound(py, elements)?.unbind()) + } + + /// Return a dictionary of circuit properties. + fn properties(&self, py: Python) -> PyResult> { + Ok(HashMap::from_iter([ + ("size", self.size(py, false)?.into_py(py)), + ("depth", self.depth(py, false)?.into_py(py)), + ("width", self.width().into_py(py)), + ("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)?), + ])) + } + + /// Draws the dag circuit. + /// + /// This function needs `Graphviz `_ to be + /// installed. Graphviz is not a python package and can't be pip installed + /// (the ``graphviz`` package on PyPI is a Python interface library for + /// Graphviz and does not actually install Graphviz). You can refer to + /// `the Graphviz documentation `__ on + /// how to install it. + /// + /// Args: + /// scale (float): scaling factor + /// filename (str): file path to save image to (format inferred from name) + /// style (str): + /// 'plain': B&W graph; + /// 'color' (default): color input/output/op nodes + /// + /// Returns: + /// Ipython.display.Image: if in Jupyter notebook and not saving to file, + /// otherwise None. + #[pyo3(signature=(scale=0.7, filename=None, style="color"))] + fn draw<'py>( + slf: PyRef<'py, Self>, + py: Python<'py>, + scale: f64, + filename: Option<&str>, + style: &str, + ) -> PyResult> { + let module = PyModule::import_bound(py, "qiskit.visualization.dag_visualization")?; + module.call_method1("dag_drawer", (slf, scale, filename, style)) + } + + fn _to_dot<'py>( + &self, + py: Python<'py>, + graph_attrs: Option>, + node_attrs: Option, + edge_attrs: Option, + ) -> PyResult> { + let mut buffer = Vec::::new(); + build_dot(py, self, &mut buffer, graph_attrs, node_attrs, edge_attrs)?; + Ok(PyString::new_bound(py, std::str::from_utf8(&buffer)?)) + } + + /// Add an input variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_input_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + if !self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .is_empty() + { + return Err(DAGCircuitError::new_err( + "cannot add inputs to a circuit with captures", + )); + } + self.add_var(py, var, DAGVarType::Input) + } + + /// Add a captured variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_captured_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + if !self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .is_empty() + { + return Err(DAGCircuitError::new_err( + "cannot add captures to a circuit with inputs", + )); + } + self.add_var(py, var, DAGVarType::Capture) + } + + /// Add a declared local variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_declared_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + self.add_var(py, var, DAGVarType::Declare) + } + + /// Total number of classical variables tracked by the circuit. + #[getter] + fn num_vars(&self) -> usize { + self.vars_info.len() + } + + /// Number of input classical variables tracked by the circuit. + #[getter] + fn num_input_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Input as usize].bind(py).len() + } + + /// Number of captured classical variables tracked by the circuit. + #[getter] + fn num_captured_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .len() + } + + /// Number of declared local classical variables tracked by the circuit. + #[getter] + fn num_declared_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .len() + } + + /// Is this realtime variable in the DAG? + /// + /// Args: + /// var: the variable or name to check. + fn has_var(&self, var: &Bound) -> PyResult { + match var.extract::() { + Ok(name) => Ok(self.vars_info.contains_key(&name)), + Err(_) => { + let raw_name = var.getattr("name")?; + let var_name: String = raw_name.extract()?; + match self.vars_info.get(&var_name) { + Some(var_in_dag) => Ok(var_in_dag.var.is(var)), + None => Ok(false), + } + } + } + } + + /// Iterable over the input classical variables tracked by the circuit. + fn iter_input_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over the captured classical variables tracked by the circuit. + fn iter_captured_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over the declared classical variables tracked by the circuit. + fn iter_declared_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over all the classical variables tracked by the circuit. + fn iter_vars(&self, py: Python) -> PyResult> { + let out_set = PySet::empty_bound(py)?; + for var_type_set in &self.vars_by_type { + for var in var_type_set.bind(py).iter() { + out_set.add(var)?; + } + } + Ok(out_set.into_any().iter()?.unbind()) + } + + fn _has_edge(&self, source: usize, target: usize) -> bool { + self.dag + .contains_edge(NodeIndex::new(source), NodeIndex::new(target)) + } + + fn _is_dag(&self) -> bool { + rustworkx_core::petgraph::algo::toposort(&self.dag, None).is_ok() + } + + fn _in_edges(&self, py: Python, node_index: usize) -> Vec> { + self.dag + .edges_directed(NodeIndex::new(node_index), Incoming) + .map(|wire| { + ( + wire.source().index(), + wire.target().index(), + match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + ) + .into_py(py) + }) + .collect() + } + + fn _out_edges(&self, py: Python, node_index: usize) -> Vec> { + self.dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .map(|wire| { + ( + wire.source().index(), + wire.target().index(), + match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + ) + .into_py(py) + }) + .collect() + } + + fn _in_wires(&self, node_index: usize) -> Vec<&PyObject> { + self.dag + .edges_directed(NodeIndex::new(node_index), Incoming) + .map(|wire| match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }) + .collect() + } + + fn _out_wires(&self, node_index: usize) -> Vec<&PyObject> { + self.dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .map(|wire| match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }) + .collect() + } + + fn _find_successors_by_edge( + &self, + py: Python, + node_index: usize, + edge_checker: &Bound, + ) -> PyResult> { + let mut result = Vec::new(); + for e in self + .dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .unique_by(|e| e.id()) + { + let weight = match e.weight() { + Wire::Qubit(q) => self.qubits.get(*q).unwrap(), + Wire::Clbit(c) => self.clbits.get(*c).unwrap(), + Wire::Var(v) => v, + }; + if edge_checker.call1((weight,))?.extract::()? { + result.push(self.get_node(py, e.target())?); + } + } + Ok(result) + } + + fn _insert_1q_on_incoming_qubit( + &mut self, + py: Python, + node: &Bound, + old_index: usize, + ) -> PyResult<()> { + if let NodeType::Operation(inst) = self.pack_into(py, node)? { + self.increment_op(inst.op.name()); + let new_index = self.dag.add_node(NodeType::Operation(inst)); + let old_index: NodeIndex = NodeIndex::new(old_index); + let (parent_index, edge_index, weight) = self + .dag + .edges_directed(old_index, Incoming) + .map(|edge| (edge.source(), edge.id(), edge.weight().clone())) + .next() + .unwrap(); + self.dag.add_edge(parent_index, new_index, weight.clone()); + self.dag.add_edge(new_index, old_index, weight); + self.dag.remove_edge(edge_index); + Ok(()) + } else { + Err(PyTypeError::new_err("Invalid node type input")) + } + } + + fn _edges(&self, py: Python) -> Vec { + self.dag + .edge_indices() + .map(|index| { + let wire = self.dag.edge_weight(index).unwrap(); + match wire { + Wire::Qubit(qubit) => self.qubits.get(*qubit).to_object(py), + Wire::Clbit(clbit) => self.clbits.get(*clbit).to_object(py), + Wire::Var(var) => var.clone_ref(py), + } + }) + .collect() + } +} + +impl DAGCircuit { + /// Return an iterator of gate runs with non-conditional op nodes of given names + pub fn collect_runs( + &self, + namelist: HashSet, + ) -> Option> + '_> { + let filter_fn = move |node_index: NodeIndex| -> Result { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => Ok(namelist.contains(inst.op.name()) + && match &inst.extra_attrs { + None => true, + Some(attrs) => attrs.condition.is_none(), + }), + _ => Ok(false), + } + }; + rustworkx_core::dag_algo::collect_runs(&self.dag, filter_fn) + .map(|node_iter| node_iter.map(|x| x.unwrap())) + } + + /// Return a set of non-conditional runs of 1q "op" nodes. + pub fn collect_1q_runs(&self) -> Option> + '_> { + let filter_fn = move |node_index: NodeIndex| -> Result { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => Ok(inst.op.num_qubits() == 1 + && inst.op.num_clbits() == 0 + && !inst.is_parameterized() + && (inst.op.try_standard_gate().is_some() + || inst.op.matrix(inst.params_view()).is_some()) + && inst.condition().is_none()), + _ => Ok(false), + } + }; + rustworkx_core::dag_algo::collect_runs(&self.dag, filter_fn) + .map(|node_iter| node_iter.map(|x| x.unwrap())) + } + + /// Return a set of non-conditional runs of 2q "op" nodes. + pub fn collect_2q_runs(&self) -> Option>> { + let filter_fn = move |node_index: NodeIndex| -> Result, Infallible> { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => match inst.op.view() { + OperationRef::Standard(gate) => Ok(Some( + gate.num_qubits() <= 2 + && inst.condition().is_none() + && !inst.is_parameterized(), + )), + OperationRef::Gate(gate) => Ok(Some( + gate.num_qubits() <= 2 + && inst.condition().is_none() + && !inst.is_parameterized(), + )), + _ => Ok(Some(false)), + }, + _ => Ok(None), + } + }; + + let color_fn = move |edge_index: EdgeIndex| -> Result, Infallible> { + let wire = self.dag.edge_weight(edge_index).unwrap(); + match wire { + Wire::Qubit(index) => Ok(Some(index.0 as usize)), + _ => Ok(None), + } + }; + rustworkx_core::dag_algo::collect_bicolor_runs(&self.dag, filter_fn, color_fn).unwrap() + } + + fn increment_op(&mut self, op: &str) { + match self.op_names.get_mut(op) { + Some(count) => { + *count += 1; + } + None => { + self.op_names.insert(op.to_string(), 1); + } + } + } + + fn decrement_op(&mut self, op: &str) { + match self.op_names.get_mut(op) { + Some(count) => { + if *count > 1 { + *count -= 1; + } else { + self.op_names.swap_remove(op); + } + } + None => panic!("Cannot decrement something not added!"), + } + } + + fn quantum_predecessors(&self, node: NodeIndex) -> impl Iterator + '_ { + self.dag + .edges_directed(node, Incoming) + .filter_map(|e| match e.weight() { + Wire::Qubit(_) => Some(e.source()), + _ => None, + }) + .unique() + } + + fn quantum_successors(&self, node: NodeIndex) -> impl Iterator + '_ { + self.dag + .edges_directed(node, Outgoing) + .filter_map(|e| match e.weight() { + Wire::Qubit(_) => Some(e.target()), + _ => None, + }) + .unique() + } + + /// Apply a [PackedInstruction] to the back of the circuit. + /// + /// The provided `instr` MUST be valid for this DAG, e.g. its + /// bits, registers, vars, and interner IDs must be valid in + /// this DAG. + /// + /// This is mostly used to apply operations from one DAG to + /// another that was created from the first via + /// [DAGCircuit::copy_empty_like]. + fn push_back(&mut self, py: Python, instr: PackedInstruction) -> PyResult { + 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_cache.intern(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_cache.intern(instr.clbits).to_vec(), None) + } + }; + + self.increment_op(op_name); + + let qubits_id = instr.qubits; + let new_node = self.dag.add_node(NodeType::Operation(instr)); + + // Put the new node in-between the previously "last" nodes on each wire + // and the output map. + let output_nodes: HashSet = self + .qargs_cache + .intern(qubits_id) + .iter() + .map(|q| self.qubit_io_map.get(q.0 as usize).map(|x| x[1]).unwrap()) + .chain( + all_cbits + .iter() + .map(|c| self.clbit_io_map.get(c.0 as usize).map(|x| x[1]).unwrap()), + ) + .chain( + vars.iter() + .flatten() + .map(|v| self.var_output_map.get(py, v).unwrap()), + ) + .collect(); + + for output_node in output_nodes { + let last_edges: Vec<_> = self + .dag + .edges_directed(output_node, Incoming) + .map(|e| (e.source(), e.id(), e.weight().clone())) + .collect(); + for (source, old_edge, weight) in last_edges.into_iter() { + self.dag.add_edge(source, new_node, weight.clone()); + self.dag.add_edge(new_node, output_node, weight); + self.dag.remove_edge(old_edge); + } + } + + Ok(new_node) + } + + /// Apply a [PackedInstruction] to the front of the circuit. + /// + /// The provided `instr` MUST be valid for this DAG, e.g. its + /// bits, registers, vars, and interner IDs must be valid in + /// this DAG. + /// + /// This is mostly used to apply operations from one DAG to + /// another that was created from the first via + /// [DAGCircuit::copy_empty_like]. + fn push_front(&mut self, py: Python, inst: PackedInstruction) -> PyResult { + let op_name = inst.op.name(); + let (all_cbits, vars): (Vec, Option>) = { + if self.may_have_additional_wires(py, &inst) { + let mut clbits: HashSet = + HashSet::from_iter(self.cargs_cache.intern(inst.clbits).iter().cloned()); + let (additional_clbits, additional_vars) = + self.additional_wires(py, inst.op.view(), inst.condition())?; + for clbit in additional_clbits { + clbits.insert(clbit); + } + (clbits.into_iter().collect(), Some(additional_vars)) + } else { + (self.cargs_cache.intern(inst.clbits).to_vec(), None) + } + }; + + self.increment_op(op_name); + + let qubits_id = inst.qubits; + let new_node = self.dag.add_node(NodeType::Operation(inst)); + + // Put the new node in-between the input map and the previously + // "first" nodes on each wire. + let mut input_nodes: Vec = self + .qargs_cache + .intern(qubits_id) + .iter() + .map(|q| self.qubit_io_map[q.0 as usize][0]) + .chain(all_cbits.iter().map(|c| self.clbit_io_map[c.0 as usize][0])) + .collect(); + if let Some(vars) = vars { + for var in vars { + input_nodes.push(self.var_input_map.get(py, &var).unwrap()); + } + } + + for input_node in input_nodes { + let first_edges: Vec<_> = self + .dag + .edges_directed(input_node, Outgoing) + .map(|e| (e.target(), e.id(), e.weight().clone())) + .collect(); + for (target, old_edge, weight) in first_edges.into_iter() { + self.dag.add_edge(input_node, new_node, weight.clone()); + self.dag.add_edge(new_node, target, weight); + self.dag.remove_edge(old_edge); + } + } + + Ok(new_node) + } + + fn sort_key(&self, node: NodeIndex) -> SortKeyType { + match &self.dag[node] { + NodeType::Operation(packed) => ( + self.qargs_cache.intern(packed.qubits).as_slice(), + self.cargs_cache.intern(packed.clbits).as_slice(), + ), + NodeType::QubitIn(q) => (std::slice::from_ref(q), &[Clbit(u32::MAX)]), + NodeType::QubitOut(_q) => (&[Qubit(u32::MAX)], &[Clbit(u32::MAX)]), + NodeType::ClbitIn(c) => (&[Qubit(u32::MAX)], std::slice::from_ref(c)), + NodeType::ClbitOut(_c) => (&[Qubit(u32::MAX)], &[Clbit(u32::MAX)]), + _ => (&[], &[]), + } + } + + fn topological_nodes(&self) -> PyResult> { + let key = |node: NodeIndex| -> Result { Ok(self.sort_key(node)) }; + let nodes = + rustworkx_core::dag_algo::lexicographical_topological_sort(&self.dag, key, false, None) + .map_err(|e| match e { + rustworkx_core::dag_algo::TopologicalSortError::CycleOrBadInitialState => { + PyValueError::new_err(format!("{}", e)) + } + rustworkx_core::dag_algo::TopologicalSortError::KeyError(_) => { + unreachable!() + } + })?; + Ok(nodes.into_iter()) + } + + fn topological_op_nodes(&self) -> PyResult + '_> { + Ok(self.topological_nodes()?.filter(|node: &NodeIndex| { + matches!(self.dag.node_weight(*node), Some(NodeType::Operation(_))) + })) + } + + fn topological_key_sort( + &self, + py: Python, + key: &Bound, + ) -> PyResult> { + // This path (user provided key func) is not ideal, since we no longer + // use a string key after moving to Rust, in favor of using a tuple + // of the qargs and cargs interner IDs of the node. + let key = |node: NodeIndex| -> PyResult { + let node = self.get_node(py, node)?; + key.call1((node,))?.extract() + }; + Ok( + rustworkx_core::dag_algo::lexicographical_topological_sort(&self.dag, key, false, None) + .map_err(|e| match e { + rustworkx_core::dag_algo::TopologicalSortError::CycleOrBadInitialState => { + PyValueError::new_err(format!("{}", e)) + } + rustworkx_core::dag_algo::TopologicalSortError::KeyError(ref e) => { + e.clone_ref(py) + } + })? + .into_iter(), + ) + } + + fn is_wire_idle(&self, py: Python, wire: &Wire) -> PyResult { + let (input_node, output_node) = match wire { + Wire::Qubit(qubit) => ( + self.qubit_io_map[qubit.0 as usize][0], + self.qubit_io_map[qubit.0 as usize][1], + ), + Wire::Clbit(clbit) => ( + self.clbit_io_map[clbit.0 as usize][0], + self.clbit_io_map[clbit.0 as usize][1], + ), + Wire::Var(var) => ( + self.var_input_map.get(py, var).unwrap(), + self.var_output_map.get(py, var).unwrap(), + ), + }; + + let child = self + .dag + .neighbors_directed(input_node, Outgoing) + .next() + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Invalid dagcircuit input node {:?} has no output", + input_node + )) + })?; + + Ok(child == output_node) + } + + fn may_have_additional_wires(&self, py: Python, instr: &PackedInstruction) -> bool { + if instr.condition().is_some() { + return true; + } + let OperationRef::Instruction(inst) = instr.op.view() else { + return false; + }; + inst.control_flow() + || inst + .instruction + .bind(py) + .is_instance(imports::STORE_OP.get_bound(py)) + .unwrap() + } + + fn additional_wires( + &self, + py: Python, + op: OperationRef, + condition: Option<&PyObject>, + ) -> PyResult<(Vec, Vec)> { + let wires_from_expr = |node: &Bound| -> PyResult<(Vec, Vec)> { + let mut clbits = Vec::new(); + let mut vars = Vec::new(); + for var in imports::ITER_VARS.get_bound(py).call1((node,))?.iter()? { + let var = var?; + let var_var = var.getattr("var")?; + if var_var.is_instance(imports::CLBIT.get_bound(py))? { + clbits.push(self.clbits.find(&var_var).unwrap()); + } else if var_var.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + for bit in var_var.iter().unwrap() { + clbits.push(self.clbits.find(&bit?).unwrap()); + } + } else { + vars.push(var.unbind()); + } + } + Ok((clbits, vars)) + }; + + let mut clbits = Vec::new(); + let mut vars = Vec::new(); + if let Some(condition) = condition { + let condition = condition.bind(py); + if !condition.is_none() { + if condition.is_instance(imports::EXPR.get_bound(py)).unwrap() { + let (expr_clbits, expr_vars) = wires_from_expr(condition)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } else { + for bit in self + .control_flow_module + .condition_resources(condition)? + .clbits + .bind(py) + { + clbits.push(self.clbits.find(&bit).unwrap()); + } + } + } + } + + if let OperationRef::Instruction(inst) = op { + let op = inst.instruction.bind(py); + if inst.control_flow() { + for var in op.call_method0("iter_captured_vars")?.iter()? { + vars.push(var?.unbind()) + } + if op.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { + let target = op.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + clbits.push(self.clbits.find(&target).unwrap()); + } else if target.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + for bit in target.iter()? { + clbits.push(self.clbits.find(&bit?).unwrap()); + } + } else { + let (expr_clbits, expr_vars) = wires_from_expr(&target)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } + } + } else if op.is_instance(imports::STORE_OP.get_bound(py))? { + let (expr_clbits, expr_vars) = wires_from_expr(&op.getattr("lvalue")?)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + let (expr_clbits, expr_vars) = wires_from_expr(&op.getattr("rvalue")?)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } + } + Ok((clbits, vars)) + } + + /// Add a qubit or bit to the circuit. + /// + /// Args: + /// wire: the wire to be added + /// + /// This adds a pair of in and out nodes connected by an edge. + /// + /// Raises: + /// DAGCircuitError: if trying to add duplicate wire + fn add_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + let (in_node, out_node) = match wire { + Wire::Qubit(qubit) => { + if (qubit.0 as usize) >= self.qubit_io_map.len() { + let input_node = self.dag.add_node(NodeType::QubitIn(qubit)); + let output_node = self.dag.add_node(NodeType::QubitOut(qubit)); + self.qubit_io_map.push([input_node, output_node]); + Ok((input_node, output_node)) + } else { + Err(DAGCircuitError::new_err("qubit wire already exists!")) + } + } + Wire::Clbit(clbit) => { + if (clbit.0 as usize) >= self.clbit_io_map.len() { + let input_node = self.dag.add_node(NodeType::ClbitIn(clbit)); + let output_node = self.dag.add_node(NodeType::ClbitOut(clbit)); + self.clbit_io_map.push([input_node, output_node]); + Ok((input_node, output_node)) + } else { + Err(DAGCircuitError::new_err("classical wire already exists!")) + } + } + Wire::Var(ref var) => { + if self.var_input_map.contains_key(py, var) + || self.var_output_map.contains_key(py, var) + { + return Err(DAGCircuitError::new_err("var wire already exists!")); + } + let in_node = self.dag.add_node(NodeType::VarIn(var.clone_ref(py))); + let out_node = self.dag.add_node(NodeType::VarOut(var.clone_ref(py))); + self.var_input_map.insert(py, var.clone_ref(py), in_node); + self.var_output_map.insert(py, var.clone_ref(py), out_node); + Ok((in_node, out_node)) + } + }?; + + self.dag.add_edge(in_node, out_node, wire); + Ok(()) + } + + /// Get the nodes on the given wire. + /// + /// Note: result is empty if the wire is not in the DAG. + fn nodes_on_wire(&self, py: Python, wire: &Wire, only_ops: bool) -> Vec { + let mut nodes = Vec::new(); + let mut current_node = match wire { + Wire::Qubit(qubit) => self.qubit_io_map.get(qubit.0 as usize).map(|x| x[0]), + Wire::Clbit(clbit) => self.clbit_io_map.get(clbit.0 as usize).map(|x| x[0]), + Wire::Var(var) => self.var_input_map.get(py, var), + }; + + while let Some(node) = current_node { + if only_ops { + let node_weight = self.dag.node_weight(node).unwrap(); + if let NodeType::Operation(_) = node_weight { + nodes.push(node); + } + } else { + nodes.push(node); + } + + let edges = self.dag.edges_directed(node, Outgoing); + current_node = edges.into_iter().find_map(|edge| { + if edge.weight() == wire { + Some(edge.target()) + } else { + None + } + }); + } + nodes + } + + fn remove_idle_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + let [in_node, out_node] = match wire { + Wire::Qubit(qubit) => self.qubit_io_map[qubit.0 as usize], + Wire::Clbit(clbit) => self.clbit_io_map[clbit.0 as usize], + Wire::Var(var) => [ + self.var_input_map.remove(py, &var).unwrap(), + self.var_output_map.remove(py, &var).unwrap(), + ], + }; + self.dag.remove_node(in_node); + self.dag.remove_node(out_node); + Ok(()) + } + + fn add_qubit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + let qubit = self.qubits.add(py, bit, false)?; + self.qubit_locations.bind(py).set_item( + bit, + Py::new( + py, + BitLocations { + index: (self.qubits.len() - 1), + registers: PyList::empty_bound(py).unbind(), + }, + )?, + )?; + self.add_wire(py, Wire::Qubit(qubit))?; + Ok(qubit) + } + + fn add_clbit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + let clbit = self.clbits.add(py, bit, false)?; + self.clbit_locations.bind(py).set_item( + bit, + Py::new( + py, + BitLocations { + index: (self.clbits.len() - 1), + registers: PyList::empty_bound(py).unbind(), + }, + )?, + )?; + self.add_wire(py, Wire::Clbit(clbit))?; + Ok(clbit) + } + + pub fn get_node(&self, py: Python, node: NodeIndex) -> PyResult> { + self.unpack_into(py, node, self.dag.node_weight(node).unwrap()) + } + + /// Remove an operation node n. + /// + /// Add edges from predecessors to successors. + fn remove_op_node(&mut self, index: NodeIndex) { + let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::new(); + for (source, in_weight) in self + .dag + .edges_directed(index, Incoming) + .map(|x| (x.source(), x.weight())) + { + for (target, out_weight) in self + .dag + .edges_directed(index, 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 { + self.dag.add_edge(source, target, weight); + } + + match self.dag.remove_node(index) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => panic!("Must be called with valid operation node!"), + } + } + + /// Returns an iterator of the ancestors indices of a node. + pub fn ancestors(&self, node: NodeIndex) -> impl Iterator + '_ { + core_ancestors(&self.dag, node).filter(move |next| next != &node) + } + + /// Returns an iterator of the descendants of a node as DAGOpNodes and DAGOutNodes. + pub fn descendants(&self, node: NodeIndex) -> impl Iterator + '_ { + core_descendants(&self.dag, node).filter(move |next| next != &node) + } + + /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node + /// and [DAGNode] is its successors in BFS order. + pub fn bfs_successors( + &self, + node: NodeIndex, + ) -> impl Iterator)> + '_ { + core_bfs_successors(&self.dag, node).filter(move |(_, others)| !others.is_empty()) + } + + fn pack_into(&mut self, py: Python, b: &Bound) -> Result { + Ok(if let Ok(in_node) = b.downcast::() { + let in_node = in_node.borrow(); + let wire = in_node.wire.bind(py); + if wire.is_instance(imports::QUBIT.get_bound(py))? { + NodeType::QubitIn(self.qubits.find(wire).unwrap()) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + NodeType::ClbitIn(self.clbits.find(wire).unwrap()) + } else { + NodeType::VarIn(wire.clone().unbind()) + } + } else if let Ok(out_node) = b.downcast::() { + let out_node = out_node.borrow(); + let wire = out_node.wire.bind(py); + if wire.is_instance(imports::QUBIT.get_bound(py))? { + NodeType::QubitOut(self.qubits.find(wire).unwrap()) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + NodeType::ClbitOut(self.clbits.find(wire).unwrap()) + } else { + NodeType::VarIn(wire.clone().unbind()) + } + } else if let Ok(op_node) = b.downcast::() { + let op_node = op_node.borrow(); + let qubits = Interner::intern( + &mut self.qargs_cache, + self.qubits + .map_bits(op_node.instruction.qubits.bind(py))? + .collect(), + )?; + let clbits = Interner::intern( + &mut self.cargs_cache, + self.clbits + .map_bits(op_node.instruction.clbits.bind(py))? + .collect(), + )?; + let params = (!op_node.instruction.params.is_empty()) + .then(|| Box::new(op_node.instruction.params.clone())); + let inst = PackedInstruction { + op: op_node.instruction.operation.clone(), + qubits, + clbits, + params, + extra_attrs: op_node.instruction.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: op_node.instruction.py_op.clone(), + }; + NodeType::Operation(inst) + } else { + return Err(PyTypeError::new_err("Invalid type for DAGNode")); + }) + } + + fn unpack_into(&self, py: Python, id: NodeIndex, weight: &NodeType) -> PyResult> { + let dag_node = match weight { + NodeType::QubitIn(qubit) => Py::new( + py, + DAGInNode::new(py, id, self.qubits.get(*qubit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::QubitOut(qubit) => Py::new( + py, + DAGOutNode::new(py, id, self.qubits.get(*qubit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::ClbitIn(clbit) => Py::new( + py, + DAGInNode::new(py, id, self.clbits.get(*clbit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::ClbitOut(clbit) => Py::new( + py, + DAGOutNode::new(py, id, self.clbits.get(*clbit).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::Operation(packed) => { + let qubits = self.qargs_cache.intern(packed.qubits); + let clbits = self.cargs_cache.intern(packed.clbits); + Py::new( + py, + ( + DAGOpNode { + instruction: CircuitInstruction { + operation: packed.op.clone(), + qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits)) + .unbind(), + clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits)) + .unbind(), + params: packed.params_view().iter().cloned().collect(), + extra_attrs: packed.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: packed.py_op.clone(), + }, + sort_key: format!("{:?}", self.sort_key(id)).into_py(py), + }, + DAGNode { node: Some(id) }, + ), + )? + .into_any() + } + NodeType::VarIn(var) => { + Py::new(py, DAGInNode::new(py, id, var.clone_ref(py)))?.into_any() + } + NodeType::VarOut(var) => { + Py::new(py, DAGOutNode::new(py, id, var.clone_ref(py)))?.into_any() + } + }; + Ok(dag_node) + } + + /// Returns an iterator over all the indices that refer to an `Operation` node in the `DAGCircuit.` + pub fn op_nodes<'a>( + &'a self, + include_directives: bool, + ) -> Box + 'a> { + let node_ops_iter = self + .dag + .node_references() + .filter_map(|(node_index, node_type)| match node_type { + NodeType::Operation(ref node) => Some((node_index, node)), + _ => None, + }); + if !include_directives { + Box::new(node_ops_iter.filter_map(|(index, node)| { + if !node.op.directive() { + Some(index) + } else { + None + } + })) + } else { + Box::new(node_ops_iter.map(|(index, _)| index)) + } + } + + pub fn op_nodes_by_py_type<'a>( + &'a self, + op: &'a Bound, + include_directives: bool, + ) -> impl Iterator + 'a { + self.dag + .node_references() + .filter_map(move |(node, weight)| { + if let NodeType::Operation(ref packed) = weight { + if !include_directives && packed.op.directive() { + None + } else if packed.op.py_op_is_instance(op).unwrap() { + Some(node) + } else { + None + } + } else { + None + } + }) + } + + /// Returns an iterator over a list layers of the `DAGCircuit``. + pub fn multigraph_layers(&self, py: Python) -> impl Iterator> + '_ { + let mut first_layer: Vec<_> = self.qubit_io_map.iter().map(|x| x[0]).collect(); + first_layer.extend(self.clbit_io_map.iter().map(|x| x[0])); + first_layer.extend(self.var_input_map.values(py)); + // A DAG is by definition acyclical, therefore unwrapping the layer should never fail. + layers(&self.dag, first_layer).map(|layer| match layer { + Ok(layer) => layer, + Err(_) => unreachable!("Not a DAG."), + }) + } + + /// Returns an iterator over the first layer of the `DAGCircuit``. + pub fn front_layer<'a>(&'a self, py: Python) -> Box + 'a> { + let mut graph_layers = self.multigraph_layers(py); + graph_layers.next(); + + let next_layer = graph_layers.next(); + match next_layer { + Some(layer) => Box::new(layer.into_iter().filter(|node| { + matches!(self.dag.node_weight(*node).unwrap(), NodeType::Operation(_)) + })), + None => Box::new(vec![].into_iter()), + } + } + + fn substitute_node_with_subgraph( + &mut self, + py: Python, + node: NodeIndex, + other: &DAGCircuit, + qubit_map: &HashMap, + clbit_map: &HashMap, + var_map: &Py, + ) -> PyResult> { + if self.dag.node_weight(node).is_none() { + return Err(PyIndexError::new_err(format!( + "Specified node {} is not in this graph", + node.index() + ))); + } + + // Add wire from pred to succ if no ops on mapped wire on ``other`` + for (in_dag_wire, self_wire) in qubit_map.iter() { + let [input_node, out_node] = other.qubit_io_map[in_dag_wire.0 as usize]; + if other.dag.find_edge(input_node, out_node).is_some() { + let pred = self + .dag + .edges_directed(node, Incoming) + .find(|edge| { + if let Wire::Qubit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node, Outgoing) + .find(|edge| { + if let Wire::Qubit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + self.dag + .add_edge(pred.source(), succ.target(), Wire::Qubit(*self_wire)); + } + } + for (in_dag_wire, self_wire) in clbit_map.iter() { + let [input_node, out_node] = other.clbit_io_map[in_dag_wire.0 as usize]; + if other.dag.find_edge(input_node, out_node).is_some() { + let pred = self + .dag + .edges_directed(node, Incoming) + .find(|edge| { + if let Wire::Clbit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node, Outgoing) + .find(|edge| { + if let Wire::Clbit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + self.dag + .add_edge(pred.source(), succ.target(), Wire::Clbit(*self_wire)); + } + } + + let bound_var_map = var_map.bind(py); + let node_filter = |node: NodeIndex| -> bool { + match other.dag[node] { + NodeType::Operation(_) => !other + .dag + .edges_directed(node, petgraph::Direction::Outgoing) + .any(|edge| match edge.weight() { + Wire::Qubit(qubit) => !qubit_map.contains_key(qubit), + Wire::Clbit(clbit) => !clbit_map.contains_key(clbit), + Wire::Var(var) => !bound_var_map.contains(var).unwrap(), + }), + _ => false, + } + }; + let reverse_qubit_map: HashMap = + qubit_map.iter().map(|(x, y)| (*y, *x)).collect(); + let reverse_clbit_map: HashMap = + clbit_map.iter().map(|(x, y)| (*y, *x)).collect(); + let reverse_var_map = PyDict::new_bound(py); + for (k, v) in bound_var_map.iter() { + reverse_var_map.set_item(v, k)?; + } + // Copy nodes from other to self + let mut out_map: IndexMap = + IndexMap::with_capacity_and_hasher(other.dag.node_count(), RandomState::default()); + for old_index in other.dag.node_indices() { + if !node_filter(old_index) { + continue; + } + let mut new_node = other.dag[old_index].clone(); + if let NodeType::Operation(ref mut new_inst) = new_node { + let new_qubit_indices: Vec = other + .qargs_cache + .intern(new_inst.qubits) + .iter() + .map(|old_qubit| qubit_map[old_qubit]) + .collect(); + let new_clbit_indices: Vec = other + .cargs_cache + .intern(new_inst.clbits) + .iter() + .map(|old_clbit| clbit_map[old_clbit]) + .collect(); + new_inst.qubits = Interner::intern(&mut self.qargs_cache, new_qubit_indices)?; + new_inst.clbits = Interner::intern(&mut self.cargs_cache, new_clbit_indices)?; + self.increment_op(new_inst.op.name()); + } + let new_index = self.dag.add_node(new_node); + out_map.insert(old_index, new_index); + } + // If no nodes are copied bail here since there is nothing left + // to do. + if out_map.is_empty() { + match self.dag.remove_node(node) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => unreachable!("Must be called with valid operation node!"), + } + // Return a new empty map to clear allocation from out_map + return Ok(IndexMap::default()); + } + // Copy edges from other to self + for edge in other.dag.edge_references().filter(|edge| { + out_map.contains_key(&edge.target()) && out_map.contains_key(&edge.source()) + }) { + self.dag.add_edge( + out_map[&edge.source()], + out_map[&edge.target()], + match edge.weight() { + Wire::Qubit(qubit) => Wire::Qubit(qubit_map[qubit]), + Wire::Clbit(clbit) => Wire::Clbit(clbit_map[clbit]), + Wire::Var(var) => Wire::Var(bound_var_map.get_item(var)?.unwrap().unbind()), + }, + ); + } + // Add edges to/from node to nodes in other + let edges: Vec<(NodeIndex, NodeIndex, Wire)> = self + .dag + .edges_directed(node, Incoming) + .map(|x| (x.source(), x.target(), x.weight().clone())) + .collect(); + for (source, _target, weight) in edges { + let wire_input_id = match weight { + Wire::Qubit(qubit) => other + .qubit_io_map + .get(reverse_qubit_map[&qubit].0 as usize) + .map(|x| x[0]), + Wire::Clbit(clbit) => other + .clbit_io_map + .get(reverse_clbit_map[&clbit].0 as usize) + .map(|x| x[0]), + Wire::Var(ref var) => { + let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); + other.var_input_map.get(py, index) + } + }; + let old_index = + wire_input_id.and_then(|x| other.dag.neighbors_directed(x, Outgoing).next()); + let target_out = match old_index { + Some(old_index) => match out_map.get(&old_index) { + Some(new_index) => *new_index, + None => { + // If the index isn't in the node map we've already added the edges as + // part of the idle wire handling at the top of this method so just + // move on. + continue; + } + }, + None => continue, + }; + self.dag.add_edge(source, target_out, weight); + } + let edges: Vec<(NodeIndex, NodeIndex, Wire)> = self + .dag + .edges_directed(node, Outgoing) + .map(|x| (x.source(), x.target(), x.weight().clone())) + .collect(); + for (_source, target, weight) in edges { + let wire_output_id = match weight { + Wire::Qubit(qubit) => other + .qubit_io_map + .get(reverse_qubit_map[&qubit].0 as usize) + .map(|x| x[1]), + Wire::Clbit(clbit) => other + .clbit_io_map + .get(reverse_clbit_map[&clbit].0 as usize) + .map(|x| x[1]), + Wire::Var(ref var) => { + let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); + other.var_output_map.get(py, index) + } + }; + let old_index = + wire_output_id.and_then(|x| other.dag.neighbors_directed(x, Incoming).next()); + let source_out = match old_index { + Some(old_index) => match out_map.get(&old_index) { + Some(new_index) => *new_index, + None => { + // If the index isn't in the node map we've already added the edges as + // part of the idle wire handling at the top of this method so just + // move on. + continue; + } + }, + None => continue, + }; + self.dag.add_edge(source_out, target, weight); + } + // Remove node + if let NodeType::Operation(inst) = &self.dag[node] { + self.decrement_op(inst.op.name().to_string().as_str()); + } + self.dag.remove_node(node); + Ok(out_map) + } + + fn add_var(&mut self, py: Python, var: &Bound, type_: DAGVarType) -> PyResult<()> { + // The setup of the initial graph structure between an "in" and an "out" node is the same as + // the bit-related `_add_wire`, but this logically needs to do different bookkeeping around + // tracking the properties + if !var.getattr("standalone")?.extract::()? { + return Err(DAGCircuitError::new_err( + "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances", + )); + } + let var_name: String = var.getattr("name")?.extract::()?; + if let Some(previous) = self.vars_info.get(&var_name) { + if var.eq(previous.var.clone_ref(py))? { + return Err(DAGCircuitError::new_err("already present in the circuit")); + } + return Err(DAGCircuitError::new_err( + "cannot add var as its name shadows an existing var", + )); + } + let in_node = NodeType::VarIn(var.clone().unbind()); + let out_node = NodeType::VarOut(var.clone().unbind()); + let in_index = self.dag.add_node(in_node); + let out_index = self.dag.add_node(out_node); + self.dag + .add_edge(in_index, out_index, Wire::Var(var.clone().unbind())); + self.var_input_map + .insert(py, var.clone().unbind(), in_index); + self.var_output_map + .insert(py, var.clone().unbind(), out_index); + self.vars_by_type[type_ as usize] + .bind(py) + .add(var.clone().unbind())?; + self.vars_info.insert( + var_name, + DAGVarInfo { + var: var.clone().unbind(), + type_, + in_node: in_index, + out_node: out_index, + }, + ); + Ok(()) + } + + fn check_op_addition(&self, py: Python, inst: &PackedInstruction) -> PyResult<()> { + if let Some(condition) = inst.condition() { + self._check_condition(py, inst.op.name(), condition.bind(py))?; + } + + for b in self.qargs_cache.intern(inst.qubits) { + if self.qubit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "qubit {} not found in output map", + self.qubits.get(*b).unwrap() + ))); + } + } + + for b in self.cargs_cache.intern(inst.clbits) { + if !self.clbit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "clbit {} not found in output map", + self.clbits.get(*b).unwrap() + ))); + } + } + + if self.may_have_additional_wires(py, inst) { + let (clbits, vars) = self.additional_wires(py, inst.op.view(), inst.condition())?; + for b in clbits { + if !self.clbit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "clbit {} not found in output map", + self.clbits.get(b).unwrap() + ))); + } + } + for v in vars { + if !self.var_output_map.contains_key(py, &v) { + return Err(DAGCircuitError::new_err(format!( + "var {} not found in output map", + v + ))); + } + } + } + Ok(()) + } +} + +/// Add to global phase. Global phase can only be Float or ParameterExpression so this +/// does not handle the full possibility of parameter values. +fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { + Ok(match [phase, other] { + [Param::Float(a), Param::Float(b)] => Param::Float(a + b), + [Param::Float(a), Param::ParameterExpression(b)] => Param::ParameterExpression( + b.clone_ref(py) + .call_method1(py, intern!(py, "__radd__"), (*a,))?, + ), + [Param::ParameterExpression(a), Param::Float(b)] => Param::ParameterExpression( + a.clone_ref(py) + .call_method1(py, intern!(py, "__add__"), (*b,))?, + ), + [Param::ParameterExpression(a), Param::ParameterExpression(b)] => { + Param::ParameterExpression(a.clone_ref(py).call_method1( + py, + intern!(py, "__add__"), + (b,), + )?) + } + _ => panic!("Invalid global phase"), + }) +} + +type SortKeyType<'a> = (&'a [Qubit], &'a [Clbit]); diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 9af82b74fa5a..ccae7a8c5d82 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -12,47 +12,89 @@ #[cfg(feature = "cache_pygates")] use std::cell::OnceCell; +use std::hash::Hasher; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::QUANTUM_CIRCUIT; -use crate::operations::Operation; +use crate::operations::{Operation, Param}; +use crate::TupleLikeArg; -use numpy::IntoPyArray; +use ahash::AHasher; +use approx::relative_eq; +use rustworkx_core::petgraph::stable_graph::NodeIndex; +use numpy::IntoPyArray; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; +use pyo3::types::{PyString, PyTuple}; use pyo3::{intern, IntoPy, PyObject, PyResult, ToPyObject}; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] #[derive(Clone, Debug)] pub struct DAGNode { - #[pyo3(get, set)] - pub _node_id: isize, + pub node: Option, +} + +impl DAGNode { + #[inline] + pub fn py_nid(&self) -> isize { + self.node + .map(|node| node.index().try_into().unwrap()) + .unwrap_or(-1) + } } #[pymethods] impl DAGNode { #[new] #[pyo3(signature=(nid=-1))] - fn new(nid: isize) -> Self { - DAGNode { _node_id: nid } + fn py_new(nid: isize) -> PyResult { + Ok(DAGNode { + node: match nid { + -1 => None, + nid => { + let index: usize = match nid.try_into() { + Ok(index) => index, + Err(_) => { + return Err(PyValueError::new_err( + "Invalid node index, must be -1 or a non-negative integer", + )) + } + }; + Some(NodeIndex::new(index)) + } + }, + }) } - fn __getstate__(&self) -> isize { - self._node_id + #[getter(_node_id)] + fn get_py_node_id(&self) -> isize { + self.py_nid() } - fn __setstate__(&mut self, nid: isize) { - self._node_id = nid; + #[setter(_node_id)] + fn set_py_node_id(&mut self, nid: isize) { + self.node = match nid { + -1 => None, + nid => Some(NodeIndex::new(nid.try_into().unwrap())), + } + } + + fn __getstate__(&self) -> Option { + self.node.map(|node| node.index()) + } + + fn __setstate__(&mut self, index: Option) { + self.node = index.map(NodeIndex::new); } fn __lt__(&self, other: &DAGNode) -> bool { - self._node_id < other._node_id + self.py_nid() < other.py_nid() } fn __gt__(&self, other: &DAGNode) -> bool { - self._node_id > other._node_id + self.py_nid() > other.py_nid() } fn __str__(_self: &Bound) -> String { @@ -60,7 +102,7 @@ impl DAGNode { } fn __hash__(&self, py: Python) -> PyResult { - self._node_id.into_py(py).bind(py).hash() + self.py_nid().into_py(py).bind(py).hash() } } @@ -76,96 +118,124 @@ pub struct DAGOpNode { impl DAGOpNode { #[new] #[pyo3(signature = (op, qargs=None, cargs=None, *, dag=None))] - fn new( + pub fn py_new( py: Python, - op: &Bound, - qargs: Option<&Bound>, - cargs: Option<&Bound>, - dag: Option<&Bound>, - ) -> PyResult<(Self, DAGNode)> { - let qargs = - qargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - let cargs = - cargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - - let sort_key = match dag { - Some(dag) => { - let cache = dag - .getattr(intern!(py, "_key_cache"))? - .downcast_into_exact::()?; - let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); - match cache.get_item(&cache_key)? { - Some(key) => key, - None => { - let indices: PyResult> = qargs - .iter() - .chain(cargs.iter()) - .map(|bit| { - dag.call_method1(intern!(py, "find_bit"), (bit,))? - .getattr(intern!(py, "index")) - }) - .collect(); - let index_strs: Vec<_> = - indices?.into_iter().map(|i| format!("{:04}", i)).collect(); - let key = PyString::new_bound(py, index_strs.join(",").as_str()); - cache.set_item(&cache_key, &key)?; - key.into_any() + op: Bound, + qargs: Option, + cargs: Option, + #[allow(unused_variables)] dag: Option>, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map_or_else(|| PyTuple::empty_bound(py), |q| q.value); + let sort_key = qargs.str().unwrap().into(); + let cargs = cargs.map_or_else(|| PyTuple::empty_bound(py), |c| c.value); + let instruction = CircuitInstruction { + operation: py_op.operation, + qubits: qargs.unbind(), + clbits: cargs.unbind(), + params: py_op.params, + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + Py::new( + py, + ( + DAGOpNode { + instruction, + sort_key, + }, + DAGNode { node: None }, + ), + ) + } + + fn __hash__(slf: PyRef<'_, Self>) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write(slf.instruction.operation.name().as_bytes()); + Ok(hasher.finish()) + } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + // This check is more restrictive by design as it's intended to replace + // object identitity for set/dict membership and not be a semantic equivalence + // check. We have an implementation of that as part of `DAGCircuit.__eq__` and + // this method is specifically to ensure nodes are the same. This means things + // like parameter equality are stricter to reject things like + // Param::Float(0.1) == Param::ParameterExpression(0.1) (if the expression was + // a python parameter equivalent to a bound value). + let Ok(other) = other.downcast::() else { + return Ok(false); + }; + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + + if super_.py_nid() != other_super.py_nid() { + return Ok(false); + } + if !slf + .instruction + .operation + .py_eq(py, &borrowed_other.instruction.operation)? + { + return Ok(false); + } + let params_eq = if slf.instruction.operation.try_standard_gate().is_some() { + let mut params_eq = true; + for (a, b) in slf + .instruction + .params + .iter() + .zip(borrowed_other.instruction.params.iter()) + { + let res = match [a, b] { + [Param::Float(float_a), Param::Float(float_b)] => { + relative_eq!(float_a, float_b, max_relative = 1e-10) + } + [Param::ParameterExpression(param_a), Param::ParameterExpression(param_b)] => { + param_a.bind(py).eq(param_b)? } + [Param::Obj(param_a), Param::Obj(param_b)] => param_a.bind(py).eq(param_b)?, + _ => false, + }; + if !res { + params_eq = false; + break; } } - None => qargs.str()?.into_any(), + params_eq + } else { + // We've already evaluated the parameters are equal here via the Python space equality + // check so if we're not comparing standard gates and we've reached this point we know + // the parameters are already equal. + true }; - Ok(( - DAGOpNode { - instruction: CircuitInstruction::py_new( - op, - Some(qargs.into_any()), - Some(cargs.into_any()), - )?, - sort_key: sort_key.unbind(), - }, - DAGNode { _node_id: -1 }, - )) + + Ok(params_eq + && slf + .instruction + .qubits + .bind(py) + .eq(borrowed_other.instruction.qubits.clone_ref(py))? + && slf + .instruction + .clbits + .bind(py) + .eq(borrowed_other.instruction.clbits.clone_ref(py))?) } - #[pyo3(signature = (instruction, /, *, dag=None, deepcopy=false))] + #[pyo3(signature = (instruction, /, *, deepcopy=false))] #[staticmethod] fn from_instruction( py: Python, mut instruction: CircuitInstruction, - dag: Option<&Bound>, deepcopy: bool, ) -> PyResult { - let qargs = instruction.qubits.bind(py); - let cargs = instruction.clbits.bind(py); - - let sort_key = match dag { - Some(dag) => { - let cache = dag - .getattr(intern!(py, "_key_cache"))? - .downcast_into_exact::()?; - let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); - match cache.get_item(&cache_key)? { - Some(key) => key, - None => { - let indices: PyResult> = qargs - .iter() - .chain(cargs.iter()) - .map(|bit| { - dag.call_method1(intern!(py, "find_bit"), (bit,))? - .getattr(intern!(py, "index")) - }) - .collect(); - let index_strs: Vec<_> = - indices?.into_iter().map(|i| format!("{:04}", i)).collect(); - let key = PyString::new_bound(py, index_strs.join(",").as_str()); - cache.set_item(&cache_key, &key)?; - key.into_any() - } - } - } - None => qargs.str()?.into_any(), - }; + let sort_key = instruction.qubits.bind(py).str().unwrap().into(); if deepcopy { instruction.operation = instruction.operation.py_deepcopy(py, None)?; #[cfg(feature = "cache_pygates")] @@ -173,17 +243,16 @@ impl DAGOpNode { instruction.py_op = OnceCell::new(); } } - let base = PyClassInitializer::from(DAGNode { _node_id: -1 }); + let base = PyClassInitializer::from(DAGNode { node: None }); let sub = base.add_subclass(DAGOpNode { instruction, - sort_key: sort_key.unbind(), + sort_key, }); Ok(Py::new(py, sub)?.to_object(py)) } - fn __reduce__(slf: PyRef) -> PyResult { - let py = slf.py(); - let state = (slf.as_ref()._node_id, &slf.sort_key); + fn __reduce__(slf: PyRef, py: Python) -> PyResult { + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); Ok(( py.get_type_bound::(), ( @@ -197,8 +266,8 @@ impl DAGOpNode { } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } @@ -247,16 +316,16 @@ impl DAGOpNode { #[getter] fn num_qubits(&self) -> u32 { - self.instruction.op().num_qubits() + self.instruction.operation.num_qubits() } #[getter] fn num_clbits(&self) -> u32 { - self.instruction.op().num_clbits() + self.instruction.operation.num_clbits() } #[getter] - fn get_qargs(&self, py: Python) -> Py { + pub fn get_qargs(&self, py: Python) -> Py { self.instruction.qubits.clone_ref(py) } @@ -266,7 +335,7 @@ impl DAGOpNode { } #[getter] - fn get_cargs(&self, py: Python) -> Py { + pub fn get_cargs(&self, py: Python) -> Py { self.instruction.clbits.clone_ref(py) } @@ -278,7 +347,7 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] fn get_name(&self, py: Python) -> Py { - self.instruction.op().name().into_py(py) + self.instruction.operation.name().into_py(py) } #[getter] @@ -293,7 +362,7 @@ impl DAGOpNode { #[getter] fn matrix(&self, py: Python) -> Option { - let matrix = self.instruction.op().matrix(&self.instruction.params); + let matrix = self.instruction.operation.matrix(&self.instruction.params); matrix.map(|mat| mat.into_pyarray_bound(py).into()) } @@ -386,7 +455,7 @@ impl DAGOpNode { #[getter] fn definition<'py>(&self, py: Python<'py>) -> PyResult>> { self.instruction - .op() + .operation .definition(&self.instruction.params) .map(|data| { QUANTUM_CIRCUIT @@ -420,36 +489,69 @@ impl DAGOpNode { #[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] pub struct DAGInNode { #[pyo3(get)] - wire: PyObject, + pub wire: PyObject, #[pyo3(get)] sort_key: PyObject, } +impl DAGInNode { + pub fn new(py: Python, node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { + ( + DAGInNode { + wire, + sort_key: intern!(py, "[]").clone().into(), + }, + DAGNode { node: Some(node) }, + ) + } +} + #[pymethods] impl DAGInNode { #[new] - fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { Ok(( DAGInNode { wire, - sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + sort_key: intern!(py, "[]").clone().into(), }, - DAGNode { _node_id: -1 }, + DAGNode { node: None }, )) } fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref()._node_id, &slf.sort_key); + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); (py.get_type_bound::(), (&slf.wire,), state).into_py(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } + fn __hash__(slf: PyRef<'_, Self>, py: Python) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write_isize(slf.wire.bind(py).hash()?); + Ok(hasher.finish()) + } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + match other.downcast::() { + Ok(other) => { + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + Ok(super_.py_nid() == other_super.py_nid() + && slf.wire.bind(py).eq(borrowed_other.wire.clone_ref(py))?) + } + Err(_) => Ok(false), + } + } + /// Returns a representation of the DAGInNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!("DAGInNode(wire={})", self.wire.bind(py).repr()?)) @@ -460,38 +562,71 @@ impl DAGInNode { #[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] pub struct DAGOutNode { #[pyo3(get)] - wire: PyObject, + pub wire: PyObject, #[pyo3(get)] sort_key: PyObject, } +impl DAGOutNode { + pub fn new(py: Python, node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { + ( + DAGOutNode { + wire, + sort_key: intern!(py, "[]").clone().into(), + }, + DAGNode { node: Some(node) }, + ) + } +} + #[pymethods] impl DAGOutNode { #[new] - fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { Ok(( DAGOutNode { wire, - sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + sort_key: intern!(py, "[]").clone().into(), }, - DAGNode { _node_id: -1 }, + DAGNode { node: None }, )) } fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref()._node_id, &slf.sort_key); + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); (py.get_type_bound::(), (&slf.wire,), state).into_py(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } + fn __hash__(slf: PyRef<'_, Self>, py: Python) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write_isize(slf.wire.bind(py).hash()?); + Ok(hasher.finish()) + } + /// Returns a representation of the DAGOutNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!("DAGOutNode(wire={})", self.wire.bind(py).repr()?)) } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + match other.downcast::() { + Ok(other) => { + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + Ok(super_.py_nid() == other_super.py_nid() + && slf.wire.bind(py).eq(borrowed_other.wire.clone_ref(py))?) + } + Err(_) => Ok(false), + } + } } diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs new file mode 100644 index 000000000000..f6769c6cad11 --- /dev/null +++ b/crates/circuit/src/dot_utils.rs @@ -0,0 +1,109 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +// This module is forked from rustworkx at: +// https://github.com/Qiskit/rustworkx/blob/c4256daf96fc3c08c392450ed33bc0987cdb15ff/src/dot_utils.rs +// and has been modified to generate a dot file from a Rust DAGCircuit instead +// of a rustworkx PyGraph object + +use std::collections::BTreeMap; +use std::io::prelude::*; + +use crate::dag_circuit::{DAGCircuit, Wire}; +use pyo3::prelude::*; +use rustworkx_core::petgraph::visit::{ + EdgeRef, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef, +}; + +static TYPE: [&str; 2] = ["graph", "digraph"]; +static EDGE: [&str; 2] = ["--", "->"]; + +pub fn build_dot( + py: Python, + dag: &DAGCircuit, + file: &mut T, + graph_attrs: Option>, + node_attrs: Option, + edge_attrs: Option, +) -> PyResult<()> +where + T: Write, +{ + let graph = &dag.dag; + writeln!(file, "{} {{", TYPE[graph.is_directed() as usize])?; + if let Some(graph_attr_map) = graph_attrs { + for (key, value) in graph_attr_map.iter() { + writeln!(file, "{}={} ;", key, value)?; + } + } + + for node in graph.node_references() { + let node_weight = dag.get_node(py, node.id())?; + writeln!( + file, + "{} {};", + graph.to_index(node.id()), + attr_map_to_string(py, node_attrs.as_ref(), node_weight)? + )?; + } + for edge in graph.edge_references() { + let edge_weight = match edge.weight() { + Wire::Qubit(qubit) => dag.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => dag.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }; + writeln!( + file, + "{} {} {} {};", + graph.to_index(edge.source()), + EDGE[graph.is_directed() as usize], + graph.to_index(edge.target()), + attr_map_to_string(py, edge_attrs.as_ref(), edge_weight)? + )?; + } + writeln!(file, "}}")?; + Ok(()) +} + +static ATTRS_TO_ESCAPE: [&str; 2] = ["label", "tooltip"]; + +/// Convert an attr map to an output string +fn attr_map_to_string( + py: Python, + attrs: Option<&PyObject>, + weight: T, +) -> PyResult { + if attrs.is_none() { + return Ok("".to_string()); + } + let attr_callable = |node: T| -> PyResult> { + let res = attrs.unwrap().call1(py, (node.to_object(py),))?; + res.extract(py) + }; + + let attrs = attr_callable(weight)?; + if attrs.is_empty() { + return Ok("".to_string()); + } + let attr_string = attrs + .iter() + .map(|(key, value)| { + if ATTRS_TO_ESCAPE.contains(&key.as_str()) { + format!("{}=\"{}\"", key, value) + } else { + format!("{}={}", key, value) + } + }) + .collect::>() + .join(", "); + Ok(format!("[{}]", attr_string)) +} diff --git a/crates/circuit/src/error.rs b/crates/circuit/src/error.rs new file mode 100644 index 000000000000..b28e6b8f513b --- /dev/null +++ b/crates/circuit/src/error.rs @@ -0,0 +1,16 @@ +// 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::import_exception; + +import_exception!(qiskit.dagcircuit.exceptions, DAGCircuitError); +import_exception!(qiskit.dagcircuit.exceptions, DAGDependencyError); diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index d9d439bb4745..588471546273 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -57,6 +57,7 @@ impl ImportOnceCell { } pub static BUILTIN_LIST: ImportOnceCell = ImportOnceCell::new("builtins", "list"); +pub static BUILTIN_SET: ImportOnceCell = ImportOnceCell::new("builtins", "set"); pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.operation", "Operation"); pub static INSTRUCTION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.instruction", "Instruction"); @@ -65,6 +66,10 @@ pub static CONTROL_FLOW_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.controlflow", "ControlFlowOp"); pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit"); pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit"); +pub static QUANTUM_REGISTER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumregister", "QuantumRegister"); +pub static CLASSICAL_REGISTER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.classicalregister", "ClassicalRegister"); pub static PARAMETER_EXPRESSION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); pub static QUANTUM_CIRCUIT: ImportOnceCell = @@ -73,6 +78,17 @@ pub static SINGLETON_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate"); pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); +pub static VARIABLE_MAPPER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit._classical_resource_map", "VariableMapper"); +pub static IF_ELSE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "IfElseOp"); +pub static FOR_LOOP_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "ForLoopOp"); +pub static SWITCH_CASE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "SwitchCaseOp"); +pub static WHILE_LOOP_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "WhileLoopOp"); +pub static STORE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Store"); +pub static EXPR: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classical.expr", "Expr"); +pub static ITER_VARS: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.classical.expr", "iter_vars"); +pub static DAG_NODE: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit", "DAGNode"); pub static CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "ControlledGate"); pub static ANNOTATED_OPERATION: ImportOnceCell = @@ -80,6 +96,18 @@ pub static ANNOTATED_OPERATION: ImportOnceCell = pub static DEEPCOPY: ImportOnceCell = ImportOnceCell::new("copy", "deepcopy"); pub static QI_OPERATOR: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Operator"); pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); +pub static CIRCUIT_TO_DAG: ImportOnceCell = + ImportOnceCell::new("qiskit.converters", "circuit_to_dag"); +pub static DAG_TO_CIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.converters", "dag_to_circuit"); +pub static LEGACY_CONDITION_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_legacy_condition_eq"); +pub static CONDITION_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_condition_op_eq"); +pub static SWITCH_CASE_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_switch_case_eq"); +pub static FOR_LOOP_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_for_loop_eq"); pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID"); /// A mapping from the enum variant in crate::operations::StandardGate to the python diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index f22bb80ae052..e19f56e87a7d 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -17,39 +17,15 @@ use hashbrown::HashMap; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Index(u32); -pub enum InternerKey { - Index(Index), - Value(T), -} - -impl From for InternerKey { - fn from(value: Index) -> Self { - InternerKey::Index(value) - } -} - -pub struct InternerValue<'a, T> { - pub index: Index, - pub value: &'a T, -} - impl IntoPy for Index { fn into_py(self, py: Python<'_>) -> PyObject { self.0.into_py(py) } } -pub struct CacheFullError; - -impl From for PyErr { - fn from(_: CacheFullError) -> Self { - PyRuntimeError::new_err("The bit operands cache is full!") - } -} - /// An append-only data structure for interning generic /// Rust types. #[derive(Clone, Debug)] @@ -58,25 +34,20 @@ pub struct IndexedInterner { index_lookup: HashMap, Index>, } -pub trait Interner { - type Key; +pub trait Interner { type Output; /// Takes ownership of the provided key and returns the interned /// type. - fn intern(self, value: Self::Key) -> Self::Output; + fn intern(self, value: K) -> Self::Output; } -impl<'a, T> Interner for &'a IndexedInterner { - type Key = Index; - type Output = InternerValue<'a, T>; +impl<'a, T> Interner for &'a IndexedInterner { + type Output = &'a T; fn intern(self, index: Index) -> Self::Output { let value = self.entries.get(index.0 as usize).unwrap(); - InternerValue { - index, - value: value.as_ref(), - } + value.as_ref() } } @@ -84,35 +55,19 @@ impl<'a, T> Interner for &'a mut IndexedInterner where T: Eq + Hash, { - type Key = InternerKey; - type Output = Result, CacheFullError>; - - fn intern(self, key: Self::Key) -> Self::Output { - match key { - InternerKey::Index(index) => { - let value = self.entries.get(index.0 as usize).unwrap(); - Ok(InternerValue { - index, - value: value.as_ref(), - }) - } - InternerKey::Value(value) => { - if let Some(index) = self.index_lookup.get(&value).copied() { - Ok(InternerValue { - index, - value: self.entries.get(index.0 as usize).unwrap(), - }) - } else { - let args = Arc::new(value); - let index: Index = - Index(self.entries.len().try_into().map_err(|_| CacheFullError)?); - self.entries.push(args.clone()); - Ok(InternerValue { - index, - value: self.index_lookup.insert_unique_unchecked(args, index).0, - }) - } - } + type Output = PyResult; + + fn intern(self, key: T) -> Self::Output { + if let Some(index) = self.index_lookup.get(&key).copied() { + Ok(index) + } else { + let args = Arc::new(key); + let index: Index = Index(self.entries.len().try_into().map_err(|_| { + PyRuntimeError::new_err("The interner has run out of indices (cache is full)!") + })?); + self.entries.push(args.clone()); + self.index_lookup.insert_unique_unchecked(args, index); + Ok(index) } } } diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 739bf998a611..4ca86c2ca83c 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -13,18 +13,23 @@ pub mod bit_data; pub mod circuit_data; pub mod circuit_instruction; +pub mod dag_circuit; pub mod dag_node; +mod dot_utils; +mod error; pub mod gate_matrix; pub mod imports; +mod interner; pub mod operations; pub mod packed_instruction; pub mod parameter_table; pub mod slice; pub mod util; -mod interner; +mod rustworkx_core_vnext; use pyo3::prelude::*; +use pyo3::types::{PySequence, PyTuple}; pub type BitType = u32; #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] @@ -32,6 +37,25 @@ pub struct Qubit(pub BitType); #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] pub struct Clbit(pub BitType); +pub struct TupleLikeArg<'py> { + value: Bound<'py, PyTuple>, +} + +impl<'py> FromPyObject<'py> for TupleLikeArg<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let value = match ob.downcast::() { + Ok(seq) => seq.to_tuple()?, + Err(_) => PyTuple::new_bound( + ob.py(), + ob.iter()? + .map(|o| Ok(o?.unbind())) + .collect::>>()?, + ), + }; + Ok(TupleLikeArg { value }) + } +} + impl From for Qubit { fn from(value: BitType) -> Self { Qubit(value) @@ -58,11 +82,12 @@ impl From for BitType { pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 7134662e1ac6..ccf41d7eefb2 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use approx::relative_eq; use std::f64::consts::PI; use crate::circuit_data::CircuitData; @@ -35,6 +36,29 @@ pub enum Param { Obj(PyObject), } +impl Param { + pub fn eq(&self, py: Python, other: &Param) -> PyResult { + match [self, other] { + [Self::Float(a), Self::Float(b)] => Ok(a == b), + [Self::Float(a), Self::ParameterExpression(b)] => b.bind(py).eq(a), + [Self::ParameterExpression(a), Self::Float(b)] => a.bind(py).eq(b), + [Self::ParameterExpression(a), Self::ParameterExpression(b)] => a.bind(py).eq(b), + [Self::Obj(_), Self::Float(_)] => Ok(false), + [Self::Float(_), Self::Obj(_)] => Ok(false), + [Self::Obj(a), Self::ParameterExpression(b)] => a.bind(py).eq(b), + [Self::Obj(a), Self::Obj(b)] => a.bind(py).eq(b), + [Self::ParameterExpression(a), Self::Obj(b)] => a.bind(py).eq(b), + } + } + + pub fn is_close(&self, py: Python, other: &Param, max_relative: f64) -> PyResult { + match [self, other] { + [Self::Float(a), Self::Float(b)] => Ok(relative_eq!(a, b, max_relative = max_relative)), + _ => self.eq(py, other), + } + } +} + impl<'py> FromPyObject<'py> for Param { fn extract_bound(b: &Bound<'py, PyAny>) -> Result { Ok(if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? { @@ -131,6 +155,7 @@ pub trait Operation { /// `PackedInstruction::op`, and in turn is a view object onto a `PackedOperation`. /// /// This is the main way that we interact immutably with general circuit operations from Rust space. +#[derive(Debug)] pub enum OperationRef<'a> { Standard(StandardGate), Gate(&'a PyGate), diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 9c4f19aa1ba3..7619ecb9c525 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -16,13 +16,18 @@ use std::ptr::NonNull; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyType}; +use ndarray::Array2; +use num_complex::Complex64; use smallvec::SmallVec; +use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; -use crate::imports::DEEPCOPY; -use crate::operations::{OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate}; +use crate::imports::{get_std_gate_class, DEEPCOPY}; +use crate::operations::{ + Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, +}; /// The logical discriminant of `PackedOperation`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -331,6 +336,78 @@ impl PackedOperation { .into()), } } + + /// Whether the Python class that we would use to represent the inner `Operation` object in + /// Python space would be an instance of the given Python type. This does not construct the + /// Python-space `Operator` instance if it can be avoided (i.e. for standard gates). + pub fn py_op_is_instance(&self, py_type: &Bound) -> PyResult { + let py = py_type.py(); + let py_op = match self.view() { + OperationRef::Standard(standard) => { + return get_std_gate_class(py, standard)? + .bind(py) + .downcast::()? + .is_subclass(py_type) + } + OperationRef::Gate(gate) => gate.gate.bind(py), + OperationRef::Instruction(instruction) => instruction.instruction.bind(py), + OperationRef::Operation(operation) => operation.operation.bind(py), + }; + py_op.is_instance(py_type) + } +} + +impl Operation for PackedOperation { + fn name(&self) -> &str { + let view = self.view(); + let name = match view { + OperationRef::Standard(ref standard) => standard.name(), + OperationRef::Gate(gate) => gate.name(), + OperationRef::Instruction(instruction) => instruction.name(), + OperationRef::Operation(operation) => operation.name(), + }; + // SAFETY: all of the inner parts of the view are owned by `self`, so it's valid for us to + // forcibly reborrowing up to our own lifetime. We avoid using `` + // just to avoid a further _potential_ unsafeness, were its implementation to start doing + // something weird with the lifetimes. `str::from_utf8_unchecked` and + // `slice::from_raw_parts` are both trivially safe because they're being called on immediate + // values from a validated `str`. + unsafe { + ::std::str::from_utf8_unchecked(::std::slice::from_raw_parts(name.as_ptr(), name.len())) + } + } + #[inline] + fn num_qubits(&self) -> u32 { + self.view().num_qubits() + } + #[inline] + fn num_clbits(&self) -> u32 { + self.view().num_clbits() + } + #[inline] + fn num_params(&self) -> u32 { + self.view().num_params() + } + #[inline] + fn control_flow(&self) -> bool { + self.view().control_flow() + } + #[inline] + fn matrix(&self, params: &[Param]) -> Option> { + self.view().matrix(params) + } + #[inline] + fn definition(&self, params: &[Param]) -> Option { + self.view().definition(params) + } + #[inline] + fn standard_gate(&self) -> Option { + self.view().standard_gate() + } + #[inline] + fn directive(&self) -> bool { + self.view().directive() + } } impl From for PackedOperation { @@ -435,15 +512,6 @@ pub struct PackedInstruction { } impl PackedInstruction { - /// Immutably view the contained operation. - /// - /// If you only care whether the contained operation is a `StandardGate` or not, you can use - /// `PackedInstruction::standard_gate`, which is a bit cheaper than this function. - #[inline] - pub fn op(&self) -> OperationRef { - self.op.view() - } - /// Access the standard gate in this `PackedInstruction`, if it is one. If the instruction /// refers to a Python-space object, `None` is returned. #[inline] @@ -469,6 +537,20 @@ impl PackedInstruction { .unwrap_or(&mut []) } + /// Does this instruction contain any compile-time symbolic `ParameterExpression`s? + pub fn is_parameterized(&self) -> bool { + self.params_view() + .iter() + .any(|x| matches!(x, Param::ParameterExpression(_))) + } + + #[inline] + pub fn condition(&self) -> Option<&Py> { + self.extra_attrs + .as_ref() + .and_then(|extra| extra.condition.as_ref()) + } + /// 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. @@ -510,4 +592,30 @@ impl PackedInstruction { } Ok(out) } + + /// Check equality of the operation, including Python-space checks, if appropriate. + pub fn py_op_eq(&self, py: Python, other: &Self) -> PyResult { + match (self.op.view(), other.op.view()) { + (OperationRef::Standard(left), OperationRef::Standard(right)) => Ok(left == right), + (OperationRef::Gate(left), OperationRef::Gate(right)) => { + left.gate.bind(py).eq(&right.gate) + } + (OperationRef::Instruction(left), OperationRef::Instruction(right)) => { + left.instruction.bind(py).eq(&right.instruction) + } + (OperationRef::Operation(left), OperationRef::Operation(right)) => { + left.operation.bind(py).eq(&right.operation) + } + // Handle the case we end up with a pygate for a standard gate + // this typically only happens if it's a ControlledGate in python + // and we have mutable state set. + (OperationRef::Standard(_left), OperationRef::Gate(right)) => { + self.unpack_py_op(py)?.bind(py).eq(&right.gate) + } + (OperationRef::Gate(left), OperationRef::Standard(_right)) => { + other.unpack_py_op(py)?.bind(py).eq(&left.gate) + } + _ => Ok(false), + } + } } diff --git a/crates/circuit/src/rustworkx_core_vnext.rs b/crates/circuit/src/rustworkx_core_vnext.rs new file mode 100644 index 000000000000..e69ee88a93e7 --- /dev/null +++ b/crates/circuit/src/rustworkx_core_vnext.rs @@ -0,0 +1,1417 @@ +// 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. + +// TODO: delete once we move to a version of Rustworkx which includes +// this implementation as part of rustworkx-core. +// PR: https://github.com/Qiskit/rustworkx/pull/1235 +pub mod isomorphism { + pub mod vf2 { + #![allow(clippy::too_many_arguments)] + // This module was originally forked from petgraph's isomorphism module @ v0.5.0 + // to handle PyDiGraph inputs instead of petgraph's generic Graph. However it has + // since diverged significantly from the original petgraph implementation. + + use std::cmp::{Ordering, Reverse}; + use std::convert::Infallible; + use std::error::Error; + use std::fmt::{Debug, Display, Formatter}; + use std::iter::Iterator; + use std::marker; + use std::ops::Deref; + + use hashbrown::HashMap; + use rustworkx_core::dictmap::*; + + use rustworkx_core::petgraph::data::{Build, Create, DataMap}; + use rustworkx_core::petgraph::stable_graph::NodeIndex; + use rustworkx_core::petgraph::visit::{ + Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, + IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, + NodeCount, NodeIndexable, + }; + use rustworkx_core::petgraph::{Directed, Incoming, Outgoing}; + + use rayon::slice::ParallelSliceMut; + + /// Returns `true` if we can map every element of `xs` to a unique + /// element of `ys` while using `matcher` func to compare two elements. + fn is_subset( + xs: &[T1], + ys: &[T2], + matcher: &mut F, + ) -> Result + where + F: FnMut(T1, T2) -> Result, + { + let mut valid = vec![true; ys.len()]; + for &a in xs { + let mut found = false; + for (&b, free) in ys.iter().zip(valid.iter_mut()) { + if *free && matcher(a, b)? { + found = true; + *free = false; + break; + } + } + + if !found { + return Ok(false); + } + } + + Ok(true) + } + + #[inline] + fn sorted(x: &mut (N, N)) { + let (a, b) = x; + if b < a { + std::mem::swap(a, b) + } + } + + /// Returns the adjacency matrix of a graph as a dictionary + /// with `(i, j)` entry equal to number of edges from node `i` to node `j`. + fn adjacency_matrix(graph: G) -> HashMap<(NodeIndex, NodeIndex), usize> + where + G: GraphProp + GraphBase + EdgeCount + IntoEdgeReferences, + { + let mut matrix = HashMap::with_capacity(graph.edge_count()); + for edge in graph.edge_references() { + let mut item = (edge.source(), edge.target()); + if !graph.is_directed() { + sorted(&mut item); + } + let entry = matrix.entry(item).or_insert(0); + *entry += 1; + } + matrix + } + + /// Returns the number of edges from node `a` to node `b`. + fn edge_multiplicity( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, + ) -> usize + where + G: GraphProp + GraphBase, + { + let mut item = (a, b); + if !graph.is_directed() { + sorted(&mut item); + } + *matrix.get(&item).unwrap_or(&0) + } + + /// Nodes `a`, `b` are adjacent if the number of edges + /// from node `a` to node `b` is greater than `val`. + fn is_adjacent( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, + val: usize, + ) -> bool + where + G: GraphProp + GraphBase, + { + edge_multiplicity(graph, matrix, a, b) >= val + } + + trait NodeSorter + where + G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph: GraphBase + + Create + + Data; + + fn sort(&self, _: G) -> Vec; + + fn reorder(&self, graph: G) -> (Self::OutputGraph, HashMap) { + let order = self.sort(graph); + + let mut new_graph = + Self::OutputGraph::with_capacity(graph.node_count(), graph.edge_count()); + let mut id_map: HashMap = + HashMap::with_capacity(graph.node_count()); + for node_index in order { + let node_data = graph.node_weight(node_index).unwrap(); + let new_index = new_graph.add_node(node_data.clone()); + id_map.insert(node_index, new_index); + } + for edge in graph.edge_references() { + let edge_w = edge.weight(); + let p_index = id_map[&edge.source()]; + let c_index = id_map[&edge.target()]; + new_graph.add_edge(p_index, c_index, edge_w.clone()); + } + ( + new_graph, + id_map.iter().map(|(k, v)| (v.index(), k.index())).collect(), + ) + } + } + + /// Sort nodes based on node ids. + struct DefaultIdSorter {} + + impl DefaultIdSorter { + pub fn new() -> Self { + Self {} + } + } + + impl NodeSorter for DefaultIdSorter + where + G: Deref + + GraphBase + + DataMap + + NodeCount + + EdgeCount + + IntoEdgeReferences + + IntoNodeIdentifiers, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + graph.node_identifiers().collect() + } + } + + /// Sort nodes based on VF2++ heuristic. + struct Vf2ppSorter {} + + impl Vf2ppSorter { + pub fn new() -> Self { + Self {} + } + } + + impl NodeSorter for Vf2ppSorter + where + G: Deref + + GraphProp + + GraphBase + + DataMap + + NodeCount + + NodeIndexable + + EdgeCount + + IntoNodeIdentifiers + + IntoEdgesDirected, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + let n = graph.node_bound(); + + let dout: Vec = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Outgoing) + .count() + }) + .collect(); + + let mut din: Vec = vec![0; n]; + if graph.is_directed() { + din = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Incoming) + .count() + }) + .collect(); + } + + let mut conn_in: Vec = vec![0; n]; + let mut conn_out: Vec = vec![0; n]; + + let mut order: Vec = Vec::with_capacity(n); + + // Process BFS level + let mut process = |mut vd: Vec| -> Vec { + // repeatedly bring largest element in front. + for i in 0..vd.len() { + let (index, &item) = vd[i..] + .iter() + .enumerate() + .max_by_key(|&(_, &node)| { + ( + conn_in[node], + dout[node], + conn_out[node], + din[node], + Reverse(node), + ) + }) + .unwrap(); + + vd.swap(i, i + index); + order.push(NodeIndex::new(item)); + + for neigh in graph.neighbors_directed(graph.from_index(item), Outgoing) { + conn_in[graph.to_index(neigh)] += 1; + } + + if graph.is_directed() { + for neigh in graph.neighbors_directed(graph.from_index(item), Incoming) + { + conn_out[graph.to_index(neigh)] += 1; + } + } + } + vd + }; + + let mut seen: Vec = vec![false; n]; + + // Create BFS Tree from root and process each level. + let mut bfs_tree = |root: usize| { + if seen[root] { + return; + } + + let mut next_level: Vec = Vec::new(); + + seen[root] = true; + next_level.push(root); + while !next_level.is_empty() { + let this_level = next_level; + let this_level = process(this_level); + + next_level = Vec::new(); + for bfs_node in this_level { + for neighbor in + graph.neighbors_directed(graph.from_index(bfs_node), Outgoing) + { + let neigh = graph.to_index(neighbor); + if !seen[neigh] { + seen[neigh] = true; + next_level.push(neigh); + } + } + } + } + }; + + let mut sorted_nodes: Vec = + graph.node_identifiers().map(|node| node.index()).collect(); + sorted_nodes.par_sort_by_key(|&node| (dout[node], din[node], Reverse(node))); + sorted_nodes.reverse(); + + for node in sorted_nodes { + bfs_tree(node); + } + + order + } + } + + #[derive(Debug)] + pub struct Vf2State { + pub graph: G, + /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, + /// NodeIndex::end() for no mapping. + mapping: Vec, + /// out[i] is non-zero if i is in either M_0(s) or Tout_0(s) + /// These are all the next vertices that are not mapped yet, but + /// have an outgoing edge from the mapping. + out: Vec, + /// ins[i] is non-zero if i is in either M_0(s) or Tin_0(s) + /// These are all the incoming vertices, those not mapped yet, but + /// have an edge from them into the mapping. + /// Unused if graph is undirected -- it's identical with out in that case. + ins: Vec, + out_size: usize, + ins_size: usize, + adjacency_matrix: HashMap<(NodeIndex, NodeIndex), usize>, + generation: usize, + _etype: marker::PhantomData, + } + + impl Vf2State + where + G: GraphBase + GraphProp + NodeCount + EdgeCount, + for<'a> &'a G: GraphBase + + GraphProp + + NodeCount + + EdgeCount + + IntoEdgesDirected, + { + pub fn new(graph: G) -> Self { + let c0 = graph.node_count(); + let is_directed = graph.is_directed(); + let adjacency_matrix = adjacency_matrix(&graph); + Vf2State { + graph, + mapping: vec![NodeIndex::end(); c0], + out: vec![0; c0], + ins: vec![0; c0 * (is_directed as usize)], + out_size: 0, + ins_size: 0, + adjacency_matrix, + generation: 0, + _etype: marker::PhantomData, + } + } + + /// Return **true** if we have a complete mapping + pub fn is_complete(&self) -> bool { + self.generation == self.mapping.len() + } + + /// Add mapping **from** <-> **to** to the state. + pub fn push_mapping(&mut self, from: NodeIndex, to: NodeIndex) { + self.generation += 1; + let s = self.generation; + self.mapping[from.index()] = to; + // update T0 & T1 ins/outs + // T0out: Node in G0 not in M0 but successor of a node in M0. + // st.out[0]: Node either in M0 or successor of M0 + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == 0 { + self.out[ix.index()] = s; + self.out_size += 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == 0 { + self.ins[ix.index()] = s; + self.ins_size += 1; + } + } + } + } + + /// Restore the state to before the last added mapping + pub fn pop_mapping(&mut self, from: NodeIndex) { + let s = self.generation; + self.generation -= 1; + + // undo (n, m) mapping + self.mapping[from.index()] = NodeIndex::end(); + + // unmark in ins and outs + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == s { + self.out[ix.index()] = 0; + self.out_size -= 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == s { + self.ins[ix.index()] = 0; + self.ins_size -= 1; + } + } + } + } + + /// Find the next (least) node in the Tout set. + pub fn next_out_index(&self, from_index: usize) -> Option { + self.out[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the Tin set. + pub fn next_in_index(&self, from_index: usize) -> Option { + self.ins[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the N - M set. + pub fn next_rest_index(&self, from_index: usize) -> Option { + self.mapping[from_index..] + .iter() + .enumerate() + .find(|&(_, elt)| *elt == NodeIndex::end()) + .map(|(index, _)| index) + } + } + + #[derive(Debug)] + pub enum IsIsomorphicError { + NodeMatcherErr(NME), + EdgeMatcherErr(EME), + } + + impl Display for IsIsomorphicError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + IsIsomorphicError::NodeMatcherErr(e) => { + write!(f, "Node match callback failed with: {}", e) + } + IsIsomorphicError::EdgeMatcherErr(e) => { + write!(f, "Edge match callback failed with: {}", e) + } + } + } + } + + impl Error for IsIsomorphicError {} + + pub struct NoSemanticMatch; + + pub trait NodeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result; + } + + impl NodeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result { + Ok(true) + } + } + + impl NodeMatcher for F + where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> Result, + { + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + n0: G0::NodeId, + n1: G1::NodeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.node_weight(n0), g1.node_weight(n1)) { + self(x, y) + } else { + Ok(false) + } + } + } + + pub trait EdgeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result; + } + + impl EdgeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _e0: G0::EdgeId, + _e1: G1::EdgeId, + ) -> Result { + Ok(true) + } + } + + impl EdgeMatcher for F + where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> Result, + { + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.edge_weight(e0), g1.edge_weight(e1)) { + self(x, y) + } else { + Ok(false) + } + } + } + + /// [Graph] Return `true` if the graphs `g0` and `g1` are (sub) graph isomorphic. + /// + /// Using the VF2 algorithm, examining both syntactic and semantic + /// graph isomorphism (graph structure and matching node and edge weights). + /// + /// The graphs should not be multigraphs. + pub fn is_isomorphic( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Result> + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + if (g0.node_count().cmp(&g1.node_count()).then(ordering) != ordering) + || (g0.edge_count().cmp(&g1.edge_count()).then(ordering) != ordering) + { + return Ok(false); + } + + let mut vf2 = Vf2Algorithm::new( + g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, + ); + + match vf2.next() { + Some(Ok(_)) => Ok(true), + Some(Err(e)) => Err(e), + None => Ok(false), + } + } + + #[derive(Copy, Clone, PartialEq, Debug)] + enum OpenList { + Out, + In, + Other, + } + + #[derive(Clone, PartialEq, Debug)] + enum Frame { + Outer, + Inner { nodes: [N; 2], open_list: OpenList }, + Unwind { nodes: [N; 2], open_list: OpenList }, + } + + /// An iterator which uses the VF2(++) algorithm to produce isomorphic matches + /// between two graphs, examining both syntactic and semantic graph isomorphism + /// (graph structure and matching node and edge weights). + /// + /// The graphs should not be multigraphs. + pub struct Vf2Algorithm + where + G0: GraphBase + Data, + G1: GraphBase + Data, + NM: NodeMatcher, + EM: EdgeMatcher, + { + pub st: (Vf2State, Vf2State), + pub node_match: NM, + pub edge_match: EM, + ordering: Ordering, + induced: bool, + node_map_g0: HashMap, + node_map_g1: HashMap, + stack: Vec>, + call_limit: Option, + _counter: usize, + } + + impl Vf2Algorithm + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + pub fn new( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Self { + let (g0, node_map_g0) = if id_order { + DefaultIdSorter::new().reorder(g0) + } else { + Vf2ppSorter::new().reorder(g0) + }; + + let (g1, node_map_g1) = if id_order { + DefaultIdSorter::new().reorder(g1) + } else { + Vf2ppSorter::new().reorder(g1) + }; + + let st = (Vf2State::new(g0), Vf2State::new(g1)); + Vf2Algorithm { + st, + node_match, + edge_match, + ordering, + induced, + node_map_g0, + node_map_g1, + stack: vec![Frame::Outer], + call_limit, + _counter: 0, + } + } + + fn mapping(&self) -> DictMap { + let mut mapping: DictMap = DictMap::new(); + self.st + .1 + .mapping + .iter() + .enumerate() + .for_each(|(index, val)| { + mapping.insert(self.node_map_g0[&val.index()], self.node_map_g1[&index]); + }); + + mapping + } + + fn next_candidate( + st: &mut (Vf2State, Vf2State), + ) -> Option<(NodeIndex, NodeIndex, OpenList)> { + // Try the out list + let mut to_index = st.1.next_out_index(0); + let mut from_index = None; + let mut open_list = OpenList::Out; + + if to_index.is_some() { + from_index = st.0.next_out_index(0); + open_list = OpenList::Out; + } + // Try the in list + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_in_index(0); + + if to_index.is_some() { + from_index = st.0.next_in_index(0); + open_list = OpenList::In; + } + } + // Try the other list -- disconnected graph + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_rest_index(0); + if to_index.is_some() { + from_index = st.0.next_rest_index(0); + open_list = OpenList::Other; + } + } + match (from_index, to_index) { + (Some(n), Some(m)) => Some((NodeIndex::new(n), NodeIndex::new(m), open_list)), + // No more candidates + _ => None, + } + } + + fn next_from_ix( + st: &mut (Vf2State, Vf2State), + nx: NodeIndex, + open_list: OpenList, + ) -> Option { + // Find the next node index to try on the `from` side of the mapping + let start = nx.index() + 1; + let cand0 = match open_list { + OpenList::Out => st.0.next_out_index(start), + OpenList::In => st.0.next_in_index(start), + OpenList::Other => st.0.next_rest_index(start), + } + .map(|c| c + start); // compensate for start offset. + match cand0 { + None => None, // no more candidates + Some(ix) => { + debug_assert!(ix >= start); + Some(NodeIndex::new(ix)) + } + } + } + + fn pop_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Restore state. + st.0.pop_mapping(nodes[0]); + st.1.pop_mapping(nodes[1]); + } + + fn push_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Add mapping nx <-> mx to the state + st.0.push_mapping(nodes[0], nodes[1]); + st.1.push_mapping(nodes[1], nodes[0]); + } + + fn is_feasible( + st: &mut (Vf2State, Vf2State), + nodes: [NodeIndex; 2], + node_match: &mut NM, + edge_match: &mut EM, + ordering: Ordering, + induced: bool, + ) -> Result> { + // Check syntactic feasibility of mapping by ensuring adjacencies + // of nx map to adjacencies of mx. + // + // nx == map to => mx + // + // R_succ + // + // Check that every neighbor of nx is mapped to a neighbor of mx, + // then check the reverse, from mx to nx. Check that they have the same + // count of edges. + // + // Note: We want to check the lookahead measures here if we can, + // R_out: Equal for G0, G1: Card(Succ(G, n) ^ Tout); for both Succ and Pred + // R_in: Same with Tin + // R_new: Equal for G0, G1: Ñ n Pred(G, n); both Succ and Pred, + // Ñ is G0 - M - Tin - Tout + let end = NodeIndex::end(); + let mut succ_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + succ_count[0] += 1; + if !induced { + continue; + } + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + continue; + } + let val = + edge_multiplicity(&st.0.graph, &st.0.adjacency_matrix, nodes[0], n_neigh); + + let has_edge = + is_adjacent(&st.1.graph, &st.1.adjacency_matrix, nodes[1], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors(nodes[1]) { + succ_count[1] += 1; + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + continue; + } + let val = + edge_multiplicity(&st.1.graph, &st.1.adjacency_matrix, nodes[1], n_neigh); + + let has_edge = + is_adjacent(&st.0.graph, &st.0.adjacency_matrix, nodes[0], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering { + return Ok(false); + } + // R_pred + if st.0.graph.is_directed() { + let mut pred_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + pred_count[0] += 1; + if !induced { + continue; + } + // the self loop case is handled in outgoing + let m_neigh = st.0.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity( + &st.0.graph, + &st.0.adjacency_matrix, + n_neigh, + nodes[0], + ); + + let has_edge = is_adjacent( + &st.1.graph, + &st.1.adjacency_matrix, + m_neigh, + nodes[1], + val, + ); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + pred_count[1] += 1; + // the self loop case is handled in outgoing + let m_neigh = st.1.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity( + &st.1.graph, + &st.1.adjacency_matrix, + n_neigh, + nodes[1], + ); + + let has_edge = is_adjacent( + &st.0.graph, + &st.0.adjacency_matrix, + m_neigh, + nodes[0], + val, + ); + if !has_edge { + return Ok(false); + } + } + if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + macro_rules! field { + ($x:ident, 0) => { + $x.0 + }; + ($x:ident, 1) => { + $x.1 + }; + ($x:ident, 1 - 0) => { + $x.1 + }; + ($x:ident, 1 - 1) => { + $x.0 + }; + } + macro_rules! rule { + ($arr:ident, $j:tt, $dir:expr) => {{ + let mut count = 0; + for n_neigh in field!(st, $j).graph.neighbors_directed(nodes[$j], $dir) { + let index = n_neigh.index(); + if field!(st, $j).$arr[index] > 0 && st.$j.mapping[index] == end { + count += 1; + } + } + count + }}; + } + // R_out + if rule!(out, 0, Outgoing) + .cmp(&rule!(out, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + if st.0.graph.is_directed() + && rule!(out, 0, Incoming) + .cmp(&rule!(out, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + // R_in + if st.0.graph.is_directed() { + if rule!(ins, 0, Outgoing) + .cmp(&rule!(ins, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + + if rule!(ins, 0, Incoming) + .cmp(&rule!(ins, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + } + // R_new + if induced { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && (st.0.ins.is_empty() || st.0.ins[index] == 0) { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors(nodes[1]) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && (st.1.ins.is_empty() || st.1.ins[index] == 0) { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + if st.0.graph.is_directed() { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && st.0.ins[index] == 0 { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && st.1.ins[index] == 0 { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + } + // semantic feasibility: compare associated data for nodes + if node_match.enabled() + && !node_match + .eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) + .map_err(IsIsomorphicError::NodeMatcherErr)? + { + return Ok(false); + } + // semantic feasibility: compare associated data for edges + if edge_match.enabled() { + let mut matcher = + |g0_edge: (NodeIndex, G0::EdgeId), + g1_edge: (NodeIndex, G1::EdgeId)| + -> Result> { + let (nx, e0) = g0_edge; + let (mx, e1) = g1_edge; + if nx == mx + && edge_match + .eq(&st.0.graph, &st.1.graph, e0, e1) + .map_err(IsIsomorphicError::EdgeMatcherErr)? + { + return Ok(true); + } + Ok(false) + }; + + // Used to reverse the order of edge args to the matcher + // when checking G1 subset of G0. + #[inline] + fn reverse_args(mut f: F) -> impl FnMut(T2, T1) -> R + where + F: FnMut(T1, T2) -> R, + { + move |y, x| f(x, y) + } + + // outgoing edges + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + + // incoming edges + if st.0.graph.is_directed() { + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + } + } + Ok(true) + } + } + + impl Iterator for Vf2Algorithm + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + type Item = Result, IsIsomorphicError>; + + /// Return Some(mapping) if isomorphism is decided, else None. + fn next(&mut self) -> Option { + if (self + .st + .0 + .graph + .node_count() + .cmp(&self.st.1.graph.node_count()) + .then(self.ordering) + != self.ordering) + || (self + .st + .0 + .graph + .edge_count() + .cmp(&self.st.1.graph.edge_count()) + .then(self.ordering) + != self.ordering) + { + return None; + } + + // A "depth first" search of a valid mapping from graph 1 to graph 2 + + // F(s, n, m) -- evaluate state s and add mapping n <-> m + + // Find least T1out node (in st.out[1] but not in M[1]) + while let Some(frame) = self.stack.pop() { + match frame { + Frame::Unwind { + nodes, + open_list: ol, + } => { + Vf2Algorithm::::pop_state(&mut self.st, nodes); + + match Vf2Algorithm::::next_from_ix( + &mut self.st, + nodes[0], + ol, + ) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Outer => { + match Vf2Algorithm::::next_candidate(&mut self.st) { + None => { + if self.st.1.is_complete() { + return Some(Ok(self.mapping())); + } + continue; + } + Some((nx, mx, ol)) => { + let f = Frame::Inner { + nodes: [nx, mx], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Inner { + nodes, + open_list: ol, + } => { + let feasible = match Vf2Algorithm::::is_feasible( + &mut self.st, + nodes, + &mut self.node_match, + &mut self.edge_match, + self.ordering, + self.induced, + ) { + Ok(f) => f, + Err(e) => { + return Some(Err(e)); + } + }; + + if feasible { + Vf2Algorithm::::push_state(&mut self.st, nodes); + // Check cardinalities of Tin, Tout sets + if self + .st + .0 + .out_size + .cmp(&self.st.1.out_size) + .then(self.ordering) + == self.ordering + && self + .st + .0 + .ins_size + .cmp(&self.st.1.ins_size) + .then(self.ordering) + == self.ordering + { + self._counter += 1; + if let Some(limit) = self.call_limit { + if self._counter > limit { + return None; + } + } + let f0 = Frame::Unwind { + nodes, + open_list: ol, + }; + + self.stack.push(f0); + self.stack.push(Frame::Outer); + continue; + } + Vf2Algorithm::::pop_state(&mut self.st, nodes); + } + match Vf2Algorithm::::next_from_ix( + &mut self.st, + nodes[0], + ol, + ) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + } + } + None + } + } + } +} diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index 10a48df99778..a330b8cbd682 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -12,7 +12,8 @@ """Helper function for converting a circuit to a dag""" -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None): @@ -93,7 +94,7 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord for instruction in circuit.data: dagcircuit._apply_op_node_back( - DAGOpNode.from_instruction(instruction, dag=dagcircuit, deepcopy=copy_operations) + DAGOpNode.from_instruction(instruction, deepcopy=copy_operations) ) dagcircuit.duration = circuit.duration diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 53cbc6f8f7f1..8738c2676a14 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -20,2406 +20,5 @@ composed, and modified. Some natural properties like depth can be computed directly from the graph. """ -from __future__ import annotations -import copy -import enum -import itertools -import math -from collections import OrderedDict, defaultdict, deque, namedtuple -from collections.abc import Callable, Sequence, Generator, Iterable -from typing import Any, Literal - -import numpy as np -import rustworkx as rx - -from qiskit.circuit import ( - ControlFlowOp, - ForLoopOp, - IfElseOp, - WhileLoopOp, - SwitchCaseOp, - _classical_resource_map, - Operation, - Store, -) -from qiskit.circuit.classical import expr -from qiskit.circuit.controlflow import condition_resources, node_resources, CONTROL_FLOW_OP_NAMES -from qiskit.circuit.quantumregister import QuantumRegister, Qubit -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.circuit.gate import Gate -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit.exceptions import DAGCircuitError -from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode -from qiskit.circuit.bit import Bit -from qiskit.pulse import Schedule -from qiskit._accelerate.euler_one_qubit_decomposer import collect_1q_runs_filter -from qiskit._accelerate.convert_2q_block_matrix import collect_2q_blocks_filter - -BitLocations = namedtuple("BitLocations", ("index", "registers")) -# The allowable arguments to :meth:`DAGCircuit.copy_empty_like`'s ``vars_mode``. -_VarsMode = Literal["alike", "captures", "drop"] - - -class DAGCircuit: - """ - Quantum circuit as a directed acyclic graph. - - There are 3 types of nodes in the graph: inputs, outputs, and operations. - The nodes are connected by directed edges that correspond to qubits and - bits. - """ - - # pylint: disable=invalid-name - - def __init__(self): - """Create an empty circuit.""" - - # Circuit name. Generally, this corresponds to the name - # of the QuantumCircuit from which the DAG was generated. - self.name = None - - # Circuit metadata - self.metadata = {} - - # Cache of dag op node sort keys - self._key_cache = {} - - # Set of wire data in the DAG. A wire is an owned unit of data. Qubits are the primary - # wire type (and the only data that has _true_ wire properties from a read/write - # perspective), but clbits and classical `Var`s are too. Note: classical registers are - # _not_ wires because the individual bits are the more fundamental unit. We treat `Var`s - # as the entire wire (as opposed to individual bits of them) for scalability reasons; if a - # parametric program wants to parametrize over 16-bit angles, we can't scale to 1000s of - # those by tracking all 16 bits individually. - # - # Classical variables shouldn't be "wires"; it should be possible to have multiple reads - # without implying ordering. The initial addition of the classical variables uses the - # existing wire structure as an MVP; we expect to handle this better in a new version of the - # transpiler IR that also handles control flow more properly. - self._wires = set() - - # Map from wire to input nodes of the graph - self.input_map = OrderedDict() - - # Map from wire to output nodes of the graph - self.output_map = OrderedDict() - - # Directed multigraph whose nodes are inputs, outputs, or operations. - # Operation nodes have equal in- and out-degrees and carry - # additional data about the operation, including the argument order - # and parameter values. - # Input nodes have out-degree 1 and output nodes have in-degree 1. - # Edges carry wire labels and each operation has - # corresponding in- and out-edges with the same wire labels. - self._multi_graph = rx.PyDAG() - - # Map of qreg/creg name to Register object. - self.qregs = OrderedDict() - self.cregs = OrderedDict() - - # List of Qubit/Clbit wires that the DAG acts on. - self.qubits: list[Qubit] = [] - self.clbits: list[Clbit] = [] - - # Dictionary mapping of Qubit and Clbit instances to a tuple comprised of - # 0) corresponding index in dag.{qubits,clbits} and - # 1) a list of Register-int pairs for each Register containing the Bit and - # its index within that register. - self._qubit_indices: dict[Qubit, BitLocations] = {} - self._clbit_indices: dict[Clbit, BitLocations] = {} - # Tracking for the classical variables used in the circuit. This contains the information - # needed to insert new nodes. This is keyed by the name rather than the `Var` instance - # itself so we can ensure we don't allow shadowing or redefinition of names. - self._vars_info: dict[str, _DAGVarInfo] = {} - # Convenience stateful tracking for the individual types of nodes to allow things like - # comparisons between circuits to take place without needing to disambiguate the - # graph-specific usage information. - self._vars_by_type: dict[_DAGVarType, set[expr.Var]] = { - type_: set() for type_ in _DAGVarType - } - - self._global_phase: float | ParameterExpression = 0.0 - self._calibrations: dict[str, dict[tuple, Schedule]] = defaultdict(dict) - - self._op_names = {} - - self.duration = None - self.unit = "dt" - - @property - def wires(self): - """Return a list of the wires in order.""" - return ( - self.qubits - + self.clbits - + [var for vars in self._vars_by_type.values() for var in vars] - ) - - @property - def node_counter(self): - """ - Returns the number of nodes in the dag. - """ - return len(self._multi_graph) - - @property - def global_phase(self): - """Return the global phase of the circuit.""" - return self._global_phase - - @global_phase.setter - def global_phase(self, angle: float | ParameterExpression): - """Set the global phase of the circuit. - - Args: - angle (float, ParameterExpression) - """ - if isinstance(angle, ParameterExpression): - self._global_phase = angle - else: - # Set the phase to the [0, 2π) interval - angle = float(angle) - if not angle: - self._global_phase = 0 - else: - self._global_phase = angle % (2 * math.pi) - - @property - def calibrations(self) -> dict[str, dict[tuple, Schedule]]: - """Return calibration dictionary. - - The custom pulse definition of a given gate is of the form - {'gate_name': {(qubits, params): schedule}} - """ - return dict(self._calibrations) - - @calibrations.setter - def calibrations(self, calibrations: dict[str, dict[tuple, Schedule]]): - """Set the circuit calibration data from a dictionary of calibration definition. - - Args: - calibrations (dict): A dictionary of input in the format - {'gate_name': {(qubits, gate_params): schedule}} - """ - self._calibrations = defaultdict(dict, calibrations) - - def add_calibration(self, gate, qubits, schedule, params=None): - """Register a low-level, custom pulse definition for the given gate. - - Args: - gate (Union[Gate, str]): Gate information. - qubits (Union[int, Tuple[int]]): List of qubits to be measured. - schedule (Schedule): Schedule information. - params (Optional[List[Union[float, Parameter]]]): A list of parameters. - - Raises: - Exception: if the gate is of type string and params is None. - """ - - def _format(operand): - try: - # Using float/complex value as a dict key is not good idea. - # This makes the mapping quite sensitive to the rounding error. - # However, the mechanism is already tied to the execution model (i.e. pulse gate) - # and we cannot easily update this rule. - # The same logic exists in QuantumCircuit.add_calibration. - evaluated = complex(operand) - if np.isreal(evaluated): - evaluated = float(evaluated.real) - if evaluated.is_integer(): - evaluated = int(evaluated) - return evaluated - except TypeError: - # Unassigned parameter - return operand - - if isinstance(gate, Gate): - params = gate.params - gate = gate.name - if params is not None: - params = tuple(map(_format, params)) - else: - params = () - - self._calibrations[gate][(tuple(qubits), params)] = schedule - - def has_calibration_for(self, node): - """Return True if the dag has a calibration defined for the node operation. In this - case, the operation does not need to be translated to the device basis. - """ - if not self.calibrations or node.op.name not in self.calibrations: - return False - qubits = tuple(self.qubits.index(qubit) for qubit in node.qargs) - params = [] - for p in node.op.params: - if isinstance(p, ParameterExpression) and not p.parameters: - params.append(float(p)) - else: - params.append(p) - params = tuple(params) - return (qubits, params) in self.calibrations[node.op.name] - - def remove_all_ops_named(self, opname): - """Remove all operation nodes with the given name.""" - for n in self.named_nodes(opname): - self.remove_op_node(n) - - def add_qubits(self, qubits): - """Add individual qubit wires.""" - if any(not isinstance(qubit, Qubit) for qubit in qubits): - raise DAGCircuitError("not a Qubit instance.") - - duplicate_qubits = set(self.qubits).intersection(qubits) - if duplicate_qubits: - raise DAGCircuitError(f"duplicate qubits {duplicate_qubits}") - - for qubit in qubits: - self.qubits.append(qubit) - self._qubit_indices[qubit] = BitLocations(len(self.qubits) - 1, []) - self._add_wire(qubit) - - def add_clbits(self, clbits): - """Add individual clbit wires.""" - if any(not isinstance(clbit, Clbit) for clbit in clbits): - raise DAGCircuitError("not a Clbit instance.") - - duplicate_clbits = set(self.clbits).intersection(clbits) - if duplicate_clbits: - raise DAGCircuitError(f"duplicate clbits {duplicate_clbits}") - - for clbit in clbits: - self.clbits.append(clbit) - self._clbit_indices[clbit] = BitLocations(len(self.clbits) - 1, []) - self._add_wire(clbit) - - def add_qreg(self, qreg): - """Add all wires in a quantum register.""" - if not isinstance(qreg, QuantumRegister): - raise DAGCircuitError("not a QuantumRegister instance.") - if qreg.name in self.qregs: - raise DAGCircuitError(f"duplicate register {qreg.name}") - self.qregs[qreg.name] = qreg - existing_qubits = set(self.qubits) - for j in range(qreg.size): - if qreg[j] in self._qubit_indices: - self._qubit_indices[qreg[j]].registers.append((qreg, j)) - if qreg[j] not in existing_qubits: - self.qubits.append(qreg[j]) - self._qubit_indices[qreg[j]] = BitLocations( - len(self.qubits) - 1, registers=[(qreg, j)] - ) - self._add_wire(qreg[j]) - - def add_creg(self, creg): - """Add all wires in a classical register.""" - if not isinstance(creg, ClassicalRegister): - raise DAGCircuitError("not a ClassicalRegister instance.") - if creg.name in self.cregs: - raise DAGCircuitError(f"duplicate register {creg.name}") - self.cregs[creg.name] = creg - existing_clbits = set(self.clbits) - for j in range(creg.size): - if creg[j] in self._clbit_indices: - self._clbit_indices[creg[j]].registers.append((creg, j)) - if creg[j] not in existing_clbits: - self.clbits.append(creg[j]) - self._clbit_indices[creg[j]] = BitLocations( - len(self.clbits) - 1, registers=[(creg, j)] - ) - self._add_wire(creg[j]) - - def add_input_var(self, var: expr.Var): - """Add an input variable to the circuit. - - Args: - var: the variable to add.""" - if self._vars_by_type[_DAGVarType.CAPTURE]: - raise DAGCircuitError("cannot add inputs to a circuit with captures") - self._add_var(var, _DAGVarType.INPUT) - - def add_captured_var(self, var: expr.Var): - """Add a captured variable to the circuit. - - Args: - var: the variable to add.""" - if self._vars_by_type[_DAGVarType.INPUT]: - raise DAGCircuitError("cannot add captures to a circuit with inputs") - self._add_var(var, _DAGVarType.CAPTURE) - - def add_declared_var(self, var: expr.Var): - """Add a declared local variable to the circuit. - - Args: - var: the variable to add.""" - self._add_var(var, _DAGVarType.DECLARE) - - def _add_var(self, var: expr.Var, type_: _DAGVarType): - """Inner function to add any variable to the DAG. ``location`` should be a reference one of - the ``self._vars_*`` tracking dictionaries. - """ - # The setup of the initial graph structure between an "in" and an "out" node is the same as - # the bit-related `_add_wire`, but this logically needs to do different bookkeeping around - # tracking the properties. - if not var.standalone: - raise DAGCircuitError( - "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances" - ) - if (previous := self._vars_info.get(var.name, None)) is not None: - if previous.var == var: - raise DAGCircuitError(f"'{var}' is already present in the circuit") - raise DAGCircuitError( - f"cannot add '{var}' as its name shadows the existing '{previous.var}'" - ) - in_node = DAGInNode(wire=var) - out_node = DAGOutNode(wire=var) - in_node._node_id, out_node._node_id = self._multi_graph.add_nodes_from((in_node, out_node)) - self._multi_graph.add_edge(in_node._node_id, out_node._node_id, var) - self.input_map[var] = in_node - self.output_map[var] = out_node - self._vars_by_type[type_].add(var) - self._vars_info[var.name] = _DAGVarInfo(var, type_, in_node, out_node) - - def _add_wire(self, wire): - """Add a qubit or bit to the circuit. - - Args: - wire (Bit): the wire to be added - - This adds a pair of in and out nodes connected by an edge. - - Raises: - DAGCircuitError: if trying to add duplicate wire - """ - if wire not in self._wires: - self._wires.add(wire) - - inp_node = DAGInNode(wire=wire) - outp_node = DAGOutNode(wire=wire) - input_map_id, output_map_id = self._multi_graph.add_nodes_from([inp_node, outp_node]) - inp_node._node_id = input_map_id - outp_node._node_id = output_map_id - self.input_map[wire] = inp_node - self.output_map[wire] = outp_node - self._multi_graph.add_edge(inp_node._node_id, outp_node._node_id, wire) - else: - raise DAGCircuitError(f"duplicate wire {wire}") - - def find_bit(self, bit: Bit) -> BitLocations: - """ - Finds locations in the circuit, by mapping the Qubit and Clbit to positional index - BitLocations is defined as: BitLocations = namedtuple("BitLocations", ("index", "registers")) - - Args: - bit (Bit): The bit to locate. - - Returns: - namedtuple(int, List[Tuple(Register, int)]): A 2-tuple. The first element (``index``) - contains the index at which the ``Bit`` can be found (in either - :obj:`~DAGCircuit.qubits`, :obj:`~DAGCircuit.clbits`, depending on its - type). The second element (``registers``) is a list of ``(register, index)`` - pairs with an entry for each :obj:`~Register` in the circuit which contains the - :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). - - Raises: - DAGCircuitError: If the supplied :obj:`~Bit` was of an unknown type. - DAGCircuitError: If the supplied :obj:`~Bit` could not be found on the circuit. - """ - try: - if isinstance(bit, Qubit): - return self._qubit_indices[bit] - elif isinstance(bit, Clbit): - return self._clbit_indices[bit] - else: - raise DAGCircuitError(f"Could not locate bit of unknown type: {type(bit)}") - except KeyError as err: - raise DAGCircuitError( - f"Could not locate provided bit: {bit}. Has it been added to the DAGCircuit?" - ) from err - - def remove_clbits(self, *clbits): - """ - Remove classical bits from the circuit. All bits MUST be idle. - Any registers with references to at least one of the specified bits will - also be removed. - - Args: - clbits (List[Clbit]): The bits to remove. - - Raises: - DAGCircuitError: a clbit is not a :obj:`.Clbit`, is not in the circuit, - or is not idle. - """ - if any(not isinstance(clbit, Clbit) for clbit in clbits): - raise DAGCircuitError( - f"clbits not of type Clbit: {[b for b in clbits if not isinstance(b, Clbit)]}" - ) - - clbits = set(clbits) - unknown_clbits = clbits.difference(self.clbits) - if unknown_clbits: - raise DAGCircuitError(f"clbits not in circuit: {unknown_clbits}") - - busy_clbits = {bit for bit in clbits if not self._is_wire_idle(bit)} - if busy_clbits: - raise DAGCircuitError(f"clbits not idle: {busy_clbits}") - - # remove any references to bits - cregs_to_remove = {creg for creg in self.cregs.values() if not clbits.isdisjoint(creg)} - self.remove_cregs(*cregs_to_remove) - - for clbit in clbits: - self._remove_idle_wire(clbit) - self.clbits.remove(clbit) - del self._clbit_indices[clbit] - - # Update the indices of remaining clbits - for i, clbit in enumerate(self.clbits): - self._clbit_indices[clbit] = self._clbit_indices[clbit]._replace(index=i) - - def remove_cregs(self, *cregs): - """ - Remove classical registers from the circuit, leaving underlying bits - in place. - - Raises: - DAGCircuitError: a creg is not a ClassicalRegister, or is not in - the circuit. - """ - if any(not isinstance(creg, ClassicalRegister) for creg in cregs): - raise DAGCircuitError( - "cregs not of type ClassicalRegister: " - f"{[r for r in cregs if not isinstance(r, ClassicalRegister)]}" - ) - - unknown_cregs = set(cregs).difference(self.cregs.values()) - if unknown_cregs: - raise DAGCircuitError(f"cregs not in circuit: {unknown_cregs}") - - for creg in cregs: - del self.cregs[creg.name] - for j in range(creg.size): - bit = creg[j] - bit_position = self._clbit_indices[bit] - bit_position.registers.remove((creg, j)) - - def remove_qubits(self, *qubits): - """ - Remove quantum bits from the circuit. All bits MUST be idle. - Any registers with references to at least one of the specified bits will - also be removed. - - Args: - qubits (List[~qiskit.circuit.Qubit]): The bits to remove. - - Raises: - DAGCircuitError: a qubit is not a :obj:`~.circuit.Qubit`, is not in the circuit, - or is not idle. - """ - if any(not isinstance(qubit, Qubit) for qubit in qubits): - raise DAGCircuitError( - f"qubits not of type Qubit: {[b for b in qubits if not isinstance(b, Qubit)]}" - ) - - qubits = set(qubits) - unknown_qubits = qubits.difference(self.qubits) - if unknown_qubits: - raise DAGCircuitError(f"qubits not in circuit: {unknown_qubits}") - - busy_qubits = {bit for bit in qubits if not self._is_wire_idle(bit)} - if busy_qubits: - raise DAGCircuitError(f"qubits not idle: {busy_qubits}") - - # remove any references to bits - qregs_to_remove = {qreg for qreg in self.qregs.values() if not qubits.isdisjoint(qreg)} - self.remove_qregs(*qregs_to_remove) - - for qubit in qubits: - self._remove_idle_wire(qubit) - self.qubits.remove(qubit) - del self._qubit_indices[qubit] - - # Update the indices of remaining qubits - for i, qubit in enumerate(self.qubits): - self._qubit_indices[qubit] = self._qubit_indices[qubit]._replace(index=i) - - def remove_qregs(self, *qregs): - """ - Remove quantum registers from the circuit, leaving underlying bits - in place. - - Raises: - DAGCircuitError: a qreg is not a QuantumRegister, or is not in - the circuit. - """ - if any(not isinstance(qreg, QuantumRegister) for qreg in qregs): - raise DAGCircuitError( - f"qregs not of type QuantumRegister: " - f"{[r for r in qregs if not isinstance(r, QuantumRegister)]}" - ) - - unknown_qregs = set(qregs).difference(self.qregs.values()) - if unknown_qregs: - raise DAGCircuitError(f"qregs not in circuit: {unknown_qregs}") - - for qreg in qregs: - del self.qregs[qreg.name] - for j in range(qreg.size): - bit = qreg[j] - bit_position = self._qubit_indices[bit] - bit_position.registers.remove((qreg, j)) - - def _is_wire_idle(self, wire): - """Check if a wire is idle. - - Args: - wire (Bit): a wire in the circuit. - - Returns: - bool: true if the wire is idle, false otherwise. - - Raises: - DAGCircuitError: the wire is not in the circuit. - """ - if wire not in self._wires: - raise DAGCircuitError(f"wire {wire} not in circuit") - - try: - child = next(self.successors(self.input_map[wire])) - except StopIteration as e: - raise DAGCircuitError( - f"Invalid dagcircuit input node {self.input_map[wire]} has no output" - ) from e - return child is self.output_map[wire] - - def _remove_idle_wire(self, wire): - """Remove an idle qubit or bit from the circuit. - - Args: - wire (Bit): the wire to be removed, which MUST be idle. - """ - inp_node = self.input_map[wire] - oup_node = self.output_map[wire] - - self._multi_graph.remove_node(inp_node._node_id) - self._multi_graph.remove_node(oup_node._node_id) - self._wires.remove(wire) - del self.input_map[wire] - del self.output_map[wire] - - def _check_condition(self, name, condition): - """Verify that the condition is valid. - - Args: - name (string): used for error reporting - condition (tuple or None): a condition tuple (ClassicalRegister, int) or (Clbit, bool) - - Raises: - DAGCircuitError: if conditioning on an invalid register - """ - if condition is None: - return - resources = condition_resources(condition) - for creg in resources.cregs: - if creg.name not in self.cregs: - raise DAGCircuitError(f"invalid creg in condition for {name}") - if not set(resources.clbits).issubset(self.clbits): - raise DAGCircuitError(f"invalid clbits in condition for {name}") - - def _check_wires(self, args: Iterable[Bit | expr.Var], amap: dict[Bit | expr.Var, Any]): - """Check the values of a list of wire arguments. - - For each element of args, check that amap contains it. - - Args: - args: the elements to be checked - amap: a dictionary keyed on Qubits/Clbits - - Raises: - DAGCircuitError: if a qubit is not contained in amap - """ - # Check for each wire - for wire in args: - if wire not in amap: - raise DAGCircuitError(f"wire {wire} not found in {amap}") - - def _increment_op(self, op_name): - if op_name in self._op_names: - self._op_names[op_name] += 1 - else: - self._op_names[op_name] = 1 - - def _decrement_op(self, op_name): - if self._op_names[op_name] == 1: - del self._op_names[op_name] - else: - self._op_names[op_name] -= 1 - - def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): - """Return a copy of self with the same structure but empty. - - That structure includes: - * name and other metadata - * global phase - * duration - * all the qubits and clbits, including the registers - * all the classical variables, with a mode defined by ``vars_mode``. - - Args: - vars_mode: The mode to handle realtime variables in. - - alike - The variables in the output DAG will have the same declaration semantics as - in the original circuit. For example, ``input`` variables in the source will be - ``input`` variables in the output DAG. - - captures - All variables will be converted to captured variables. This is useful when you - are building a new layer for an existing DAG that you will want to - :meth:`compose` onto the base, since :meth:`compose` can inline captures onto - the base circuit (but not other variables). - - drop - The output DAG will have no variables defined. - - Returns: - DAGCircuit: An empty copy of self. - """ - target_dag = DAGCircuit() - target_dag.name = self.name - target_dag._global_phase = self._global_phase - target_dag.duration = self.duration - target_dag.unit = self.unit - target_dag.metadata = self.metadata - target_dag._key_cache = self._key_cache - - target_dag.add_qubits(self.qubits) - target_dag.add_clbits(self.clbits) - - for qreg in self.qregs.values(): - target_dag.add_qreg(qreg) - for creg in self.cregs.values(): - target_dag.add_creg(creg) - - if vars_mode == "alike": - for var in self.iter_input_vars(): - target_dag.add_input_var(var) - for var in self.iter_captured_vars(): - target_dag.add_captured_var(var) - for var in self.iter_declared_vars(): - target_dag.add_declared_var(var) - elif vars_mode == "captures": - for var in self.iter_vars(): - target_dag.add_captured_var(var) - elif vars_mode == "drop": - pass - else: # pragma: no cover - raise ValueError(f"unknown vars_mode: '{vars_mode}'") - - return target_dag - - def _apply_op_node_back(self, node: DAGOpNode, *, check: bool = False): - additional = () - if _may_have_additional_wires(node): - # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(node.op)).difference(node.cargs) - - if check: - self._check_condition(node.name, node.condition) - self._check_wires(node.qargs, self.output_map) - self._check_wires(node.cargs, self.output_map) - self._check_wires(additional, self.output_map) - - node._node_id = self._multi_graph.add_node(node) - self._increment_op(node.name) - - # Add new in-edges from predecessors of the output nodes to the - # operation node while deleting the old in-edges of the output nodes - # and adding new edges from the operation node to each output node - self._multi_graph.insert_node_on_in_edges_multiple( - node._node_id, - [ - self.output_map[bit]._node_id - for bits in (node.qargs, node.cargs, additional) - for bit in bits - ], - ) - return node - - def apply_operation_back( - self, - op: Operation, - qargs: Iterable[Qubit] = (), - cargs: Iterable[Clbit] = (), - *, - check: bool = True, - ) -> DAGOpNode: - """Apply an operation to the output of the circuit. - - Args: - op (qiskit.circuit.Operation): the operation associated with the DAG node - qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to - cargs (tuple[Clbit]): cbits that op will be applied to - check (bool): If ``True`` (default), this function will enforce that the - :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are - :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* - uphold these invariants itself, but the cost of several checks will be skipped. - This is most useful when building a new DAG from a source of known-good nodes. - Returns: - DAGOpNode: the node for the op that was added to the dag - - Raises: - DAGCircuitError: if a leaf node is connected to multiple outputs - - """ - return self._apply_op_node_back( - DAGOpNode(op=op, qargs=tuple(qargs), cargs=tuple(cargs), dag=self), check=check - ) - - def apply_operation_front( - self, - op: Operation, - qargs: Sequence[Qubit] = (), - cargs: Sequence[Clbit] = (), - *, - check: bool = True, - ) -> DAGOpNode: - """Apply an operation to the input of the circuit. - - Args: - op (qiskit.circuit.Operation): the operation associated with the DAG node - qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to - cargs (tuple[Clbit]): cbits that op will be applied to - check (bool): If ``True`` (default), this function will enforce that the - :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are - :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* - uphold these invariants itself, but the cost of several checks will be skipped. - This is most useful when building a new DAG from a source of known-good nodes. - Returns: - DAGOpNode: the node for the op that was added to the dag - - Raises: - DAGCircuitError: if initial nodes connected to multiple out edges - """ - qargs = tuple(qargs) - cargs = tuple(cargs) - additional = () - - node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) - if _may_have_additional_wires(node): - # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(node.op)).difference(cargs) - - if check: - self._check_condition(node.name, node.condition) - self._check_wires(node.qargs, self.output_map) - self._check_wires(node.cargs, self.output_map) - self._check_wires(additional, self.output_map) - - node._node_id = self._multi_graph.add_node(node) - self._increment_op(node.name) - - # Add new out-edges to successors of the input nodes from the - # operation node while deleting the old out-edges of the input nodes - # and adding new edges to the operation node from each input node - self._multi_graph.insert_node_on_out_edges_multiple( - node._node_id, - [ - self.input_map[bit]._node_id - for bits in (node.qargs, node.cargs, additional) - for bit in bits - ], - ) - return node - - def compose( - self, other, qubits=None, clbits=None, front=False, inplace=True, *, inline_captures=False - ): - """Compose the ``other`` circuit onto the output of this circuit. - - A subset of input wires of ``other`` are mapped - to a subset of output wires of this circuit. - - ``other`` can be narrower or of equal width to ``self``. - - Args: - other (DAGCircuit): circuit to compose with self - qubits (list[~qiskit.circuit.Qubit|int]): qubits of self to compose onto. - clbits (list[Clbit|int]): clbits of self to compose onto. - front (bool): If True, front composition will be performed (not implemented yet) - inplace (bool): If True, modify the object. Otherwise return composed circuit. - inline_captures (bool): If ``True``, variables marked as "captures" in the ``other`` DAG - will inlined onto existing uses of those same variables in ``self``. If ``False``, - all variables in ``other`` are required to be distinct from ``self``, and they will - be added to ``self``. - - .. - Note: unlike `QuantumCircuit.compose`, there's no `var_remap` argument here. That's - because the `DAGCircuit` inner-block structure isn't set up well to allow the recursion, - and `DAGCircuit.compose` is generally only used to rebuild a DAG from layers within - itself than to join unrelated circuits. While there's no strong motivating use-case - (unlike the `QuantumCircuit` equivalent), it's safer and more performant to not provide - the option. - - Returns: - DAGCircuit: the composed dag (returns None if inplace==True). - - Raises: - DAGCircuitError: if ``other`` is wider or there are duplicate edge mappings. - """ - if front: - raise DAGCircuitError("Front composition not supported yet.") - - if len(other.qubits) > len(self.qubits) or len(other.clbits) > len(self.clbits): - raise DAGCircuitError( - "Trying to compose with another DAGCircuit which has more 'in' edges." - ) - - # number of qubits and clbits must match number in circuit or None - identity_qubit_map = dict(zip(other.qubits, self.qubits)) - identity_clbit_map = dict(zip(other.clbits, self.clbits)) - if qubits is None: - qubit_map = identity_qubit_map - elif len(qubits) != len(other.qubits): - raise DAGCircuitError( - "Number of items in qubits parameter does not" - " match number of qubits in the circuit." - ) - else: - qubit_map = { - other.qubits[i]: (self.qubits[q] if isinstance(q, int) else q) - for i, q in enumerate(qubits) - } - if clbits is None: - clbit_map = identity_clbit_map - elif len(clbits) != len(other.clbits): - raise DAGCircuitError( - "Number of items in clbits parameter does not" - " match number of clbits in the circuit." - ) - else: - clbit_map = { - other.clbits[i]: (self.clbits[c] if isinstance(c, int) else c) - for i, c in enumerate(clbits) - } - edge_map = {**qubit_map, **clbit_map} or None - - # if no edge_map, try to do a 1-1 mapping in order - if edge_map is None: - edge_map = {**identity_qubit_map, **identity_clbit_map} - - # Check the edge_map for duplicate values - if len(set(edge_map.values())) != len(edge_map): - raise DAGCircuitError("duplicates in wire_map") - - # Compose - if inplace: - dag = self - else: - dag = copy.deepcopy(self) - dag.global_phase += other.global_phase - - for gate, cals in other.calibrations.items(): - dag._calibrations[gate].update(cals) - - # This is all the handling we need for realtime variables, if there's no remapping. They: - # - # * get added to the DAG and then operations involving them get appended on normally. - # * get inlined onto an existing variable, then operations get appended normally. - # * there's a clash or a failed inlining, and we just raise an error. - # - # Notably if there's no remapping, there's no need to recurse into control-flow or to do any - # Var rewriting during the Expr visits. - for var in other.iter_input_vars(): - dag.add_input_var(var) - if inline_captures: - for var in other.iter_captured_vars(): - if not dag.has_var(var): - raise DAGCircuitError( - f"Variable '{var}' to be inlined is not in the base DAG." - " If you wanted it to be automatically added, use `inline_captures=False`." - ) - else: - for var in other.iter_captured_vars(): - dag.add_captured_var(var) - for var in other.iter_declared_vars(): - dag.add_declared_var(var) - - # Ensure that the error raised here is a `DAGCircuitError` for backwards compatibility. - def _reject_new_register(reg): - raise DAGCircuitError(f"No register with '{reg.bits}' to map this expression onto.") - - variable_mapper = _classical_resource_map.VariableMapper( - dag.cregs.values(), edge_map, add_register=_reject_new_register - ) - for nd in other.topological_nodes(): - if isinstance(nd, DAGInNode): - if isinstance(nd.wire, Bit): - # if in edge_map, get new name, else use existing name - m_wire = edge_map.get(nd.wire, nd.wire) - # the mapped wire should already exist - if m_wire not in dag.output_map: - raise DAGCircuitError( - f"wire {m_wire.register.name}[{m_wire.index}] not in self" - ) - if nd.wire not in other._wires: - raise DAGCircuitError( - f"inconsistent wire type for {nd.register.name}[{nd.wire.index}] in other" - ) - # If it's a Var wire, we already checked that it exists in the destination. - elif isinstance(nd, DAGOutNode): - # ignore output nodes - pass - elif isinstance(nd, DAGOpNode): - m_qargs = [edge_map.get(x, x) for x in nd.qargs] - m_cargs = [edge_map.get(x, x) for x in nd.cargs] - inst = nd._to_circuit_instruction(deepcopy=True) - m_op = None - if inst.condition is not None: - if inst.is_control_flow(): - m_op = inst.operation - m_op.condition = variable_mapper.map_condition( - inst.condition, allow_reorder=True - ) - else: - m_op = inst.operation.c_if( - *variable_mapper.map_condition(inst.condition, allow_reorder=True) - ) - elif inst.is_control_flow() and isinstance(inst.operation, SwitchCaseOp): - m_op = inst.operation - m_op.target = variable_mapper.map_target(m_op.target) - if m_op is None: - inst = inst.replace(qubits=m_qargs, clbits=m_cargs) - else: - inst = inst.replace(operation=m_op, qubits=m_qargs, clbits=m_cargs) - dag._apply_op_node_back(DAGOpNode.from_instruction(inst), check=False) - else: - raise DAGCircuitError(f"bad node type {type(nd)}") - - if not inplace: - return dag - else: - return None - - def reverse_ops(self): - """Reverse the operations in the ``self`` circuit. - - Returns: - DAGCircuit: the reversed dag. - """ - # TODO: speed up - # pylint: disable=cyclic-import - from qiskit.converters import dag_to_circuit, circuit_to_dag - - qc = dag_to_circuit(self) - reversed_qc = qc.reverse_ops() - reversed_dag = circuit_to_dag(reversed_qc) - return reversed_dag - - def idle_wires(self, ignore=None): - """Return idle wires. - - Args: - ignore (list(str)): List of node names to ignore. Default: [] - - Yields: - Bit: Bit in idle wire. - - Raises: - DAGCircuitError: If the DAG is invalid - """ - if ignore is None: - ignore = set() - ignore_set = set(ignore) - for wire in self._wires: - if not ignore: - if self._is_wire_idle(wire): - yield wire - else: - for node in self.nodes_on_wire(wire, only_ops=True): - if node.op.name not in ignore_set: - # If we found an op node outside of ignore we can stop iterating over the wire - break - else: - yield wire - - def size(self, *, recurse: bool = False): - """Return the number of operations. If there is control flow present, this count may only - be an estimate, as the complete control-flow path cannot be statically known. - - Args: - recurse: if ``True``, then recurse into control-flow operations. For loops with - known-length iterators are counted unrolled. If-else blocks sum both of the two - branches. While loops are counted as if the loop body runs once only. Defaults to - ``False`` and raises :class:`.DAGCircuitError` if any control flow is present, to - avoid silently returning a mostly meaningless number. - - Returns: - int: the circuit size - - Raises: - DAGCircuitError: if an unknown :class:`.ControlFlowOp` is present in a call with - ``recurse=True``, or any control flow is present in a non-recursive call. - """ - length = len(self._multi_graph) - 2 * len(self._wires) - if not recurse: - if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES): - raise DAGCircuitError( - "Size with control flow is ambiguous." - " You may use `recurse=True` to get a result," - " but see this method's documentation for the meaning of this." - ) - return length - # pylint: disable=cyclic-import - from qiskit.converters import circuit_to_dag - - for node in self.op_nodes(ControlFlowOp): - if isinstance(node.op, ForLoopOp): - indexset = node.op.params[0] - inner = len(indexset) * circuit_to_dag(node.op.blocks[0]).size(recurse=True) - elif isinstance(node.op, WhileLoopOp): - inner = circuit_to_dag(node.op.blocks[0]).size(recurse=True) - elif isinstance(node.op, (IfElseOp, SwitchCaseOp)): - inner = sum(circuit_to_dag(block).size(recurse=True) for block in node.op.blocks) - else: - raise DAGCircuitError(f"unknown control-flow type: '{node.op.name}'") - # Replace the "1" for the node itself with the actual count. - length += inner - 1 - return length - - def depth(self, *, recurse: bool = False): - """Return the circuit depth. If there is control flow present, this count may only be an - estimate, as the complete control-flow path cannot be statically known. - - Args: - recurse: if ``True``, then recurse into control-flow operations. For loops - with known-length iterators are counted as if the loop had been manually unrolled - (*i.e.* with each iteration of the loop body written out explicitly). - If-else blocks take the longer case of the two branches. While loops are counted as - if the loop body runs once only. Defaults to ``False`` and raises - :class:`.DAGCircuitError` if any control flow is present, to avoid silently - returning a nonsensical number. - - Returns: - int: the circuit depth - - Raises: - DAGCircuitError: if not a directed acyclic graph - DAGCircuitError: if unknown control flow is present in a recursive call, or any control - flow is present in a non-recursive call. - """ - if recurse: - from qiskit.converters import circuit_to_dag # pylint: disable=cyclic-import - - node_lookup = {} - for node in self.op_nodes(ControlFlowOp): - weight = len(node.op.params[0]) if isinstance(node.op, ForLoopOp) else 1 - if weight == 0: - node_lookup[node._node_id] = 0 - else: - node_lookup[node._node_id] = weight * max( - circuit_to_dag(block).depth(recurse=True) for block in node.op.blocks - ) - - def weight_fn(_source, target, _edge): - return node_lookup.get(target, 1) - - else: - if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES): - raise DAGCircuitError( - "Depth with control flow is ambiguous." - " You may use `recurse=True` to get a result," - " but see this method's documentation for the meaning of this." - ) - weight_fn = None - - try: - depth = rx.dag_longest_path_length(self._multi_graph, weight_fn) - 1 - except rx.DAGHasCycle as ex: - raise DAGCircuitError("not a DAG") from ex - return depth if depth >= 0 else 0 - - def width(self): - """Return the total number of qubits + clbits used by the circuit. - This function formerly returned the number of qubits by the calculation - return len(self._wires) - self.num_clbits() - but was changed by issue #2564 to return number of qubits + clbits - with the new function DAGCircuit.num_qubits replacing the former - semantic of DAGCircuit.width(). - """ - return len(self._wires) - - def num_qubits(self): - """Return the total number of qubits used by the circuit. - num_qubits() replaces former use of width(). - DAGCircuit.width() now returns qubits + clbits for - consistency with Circuit.width() [qiskit-terra #2564]. - """ - return len(self.qubits) - - def num_clbits(self): - """Return the total number of classical bits used by the circuit.""" - return len(self.clbits) - - def num_tensor_factors(self): - """Compute how many components the circuit can decompose into.""" - return rx.number_weakly_connected_components(self._multi_graph) - - @property - def num_vars(self): - """Total number of classical variables tracked by the circuit.""" - return len(self._vars_info) - - @property - def num_input_vars(self): - """Number of input classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.INPUT]) - - @property - def num_captured_vars(self): - """Number of captured classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.CAPTURE]) - - @property - def num_declared_vars(self): - """Number of declared local classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.DECLARE]) - - def iter_vars(self): - """Iterable over all the classical variables tracked by the circuit.""" - return itertools.chain.from_iterable(self._vars_by_type.values()) - - def iter_input_vars(self): - """Iterable over the input classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.INPUT]) - - def iter_captured_vars(self): - """Iterable over the captured classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.CAPTURE]) - - def iter_declared_vars(self): - """Iterable over the declared local classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.DECLARE]) - - def has_var(self, var: str | expr.Var) -> bool: - """Is this realtime variable in the DAG? - - Args: - var: the variable or name to check. - """ - if isinstance(var, str): - return var in self._vars_info - return (info := self._vars_info.get(var.name, False)) and info.var is var - - def __eq__(self, other): - # Try to convert to float, but in case of unbound ParameterExpressions - # a TypeError will be raise, fallback to normal equality in those - # cases - try: - self_phase = float(self.global_phase) - other_phase = float(other.global_phase) - if ( - abs((self_phase - other_phase + np.pi) % (2 * np.pi) - np.pi) > 1.0e-10 - ): # TODO: atol? - return False - except TypeError: - if self.global_phase != other.global_phase: - return False - if self.calibrations != other.calibrations: - return False - - # We don't do any semantic equivalence between Var nodes, as things stand; DAGs can only be - # equal in our mind if they use the exact same UUID vars. - if self._vars_by_type != other._vars_by_type: - return False - - self_bit_indices = {bit: idx for idx, bit in enumerate(self.qubits + self.clbits)} - other_bit_indices = {bit: idx for idx, bit in enumerate(other.qubits + other.clbits)} - - self_qreg_indices = { - regname: [self_bit_indices[bit] for bit in reg] for regname, reg in self.qregs.items() - } - self_creg_indices = { - regname: [self_bit_indices[bit] for bit in reg] for regname, reg in self.cregs.items() - } - - other_qreg_indices = { - regname: [other_bit_indices[bit] for bit in reg] for regname, reg in other.qregs.items() - } - other_creg_indices = { - regname: [other_bit_indices[bit] for bit in reg] for regname, reg in other.cregs.items() - } - if self_qreg_indices != other_qreg_indices or self_creg_indices != other_creg_indices: - return False - - def node_eq(node_self, node_other): - return DAGNode.semantic_eq(node_self, node_other, self_bit_indices, other_bit_indices) - - return rx.is_isomorphic_node_match(self._multi_graph, other._multi_graph, node_eq) - - def topological_nodes(self, key=None): - """ - Yield nodes in topological order. - - Args: - key (Callable): A callable which will take a DAGNode object and - return a string sort key. If not specified the - :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be - used as the sort key for each node. - - Returns: - generator(DAGOpNode, DAGInNode, or DAGOutNode): node in topological order - """ - - def _key(x): - return x.sort_key - - if key is None: - key = _key - - return iter(rx.lexicographical_topological_sort(self._multi_graph, key=key)) - - def topological_op_nodes(self, key: Callable | None = None) -> Generator[DAGOpNode, Any, Any]: - """ - Yield op nodes in topological order. - - Allowed to pass in specific key to break ties in top order - - Args: - key (Callable): A callable which will take a DAGNode object and - return a string sort key. If not specified the - :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be - used as the sort key for each node. - - Returns: - generator(DAGOpNode): op node in topological order - """ - return (nd for nd in self.topological_nodes(key) if isinstance(nd, DAGOpNode)) - - def replace_block_with_op( - self, node_block: list[DAGOpNode], op: Operation, wire_pos_map, cycle_check=True - ): - """Replace a block of nodes with a single node. - - This is used to consolidate a block of DAGOpNodes into a single - operation. A typical example is a block of gates being consolidated - into a single ``UnitaryGate`` representing the unitary matrix of the - block. - - Args: - node_block (List[DAGNode]): A list of dag nodes that represents the - node block to be replaced - op (qiskit.circuit.Operation): The operation to replace the - block with - wire_pos_map (Dict[Bit, int]): The dictionary mapping the bits to their positions in the - output ``qargs`` or ``cargs``. This is necessary to reconstruct the arg order over - multiple gates in the combined single op node. If a :class:`.Bit` is not in the - dictionary, it will not be added to the args; this can be useful when dealing with - control-flow operations that have inherent bits in their ``condition`` or ``target`` - fields. :class:`.expr.Var` wires similarly do not need to be in this map, since - they will never be in ``qargs`` or ``cargs``. - cycle_check (bool): When set to True this method will check that - replacing the provided ``node_block`` with a single node - would introduce a cycle (which would invalidate the - ``DAGCircuit``) and will raise a ``DAGCircuitError`` if a cycle - would be introduced. This checking comes with a run time - penalty. If you can guarantee that your input ``node_block`` is - a contiguous block and won't introduce a cycle when it's - contracted to a single node, this can be set to ``False`` to - improve the runtime performance of this method. - - Raises: - DAGCircuitError: if ``cycle_check`` is set to ``True`` and replacing - the specified block introduces a cycle or if ``node_block`` is - empty. - - Returns: - DAGOpNode: The op node that replaces the block. - """ - block_qargs = set() - block_cargs = set() - block_ids = [x._node_id for x in node_block] - - # If node block is empty return early - if not node_block: - raise DAGCircuitError("Can't replace an empty node_block") - - for nd in node_block: - block_qargs |= set(nd.qargs) - block_cargs |= set(nd.cargs) - if (condition := getattr(nd, "condition", None)) is not None: - block_cargs.update(condition_resources(condition).clbits) - elif nd.name in CONTROL_FLOW_OP_NAMES and isinstance(nd.op, SwitchCaseOp): - if isinstance(nd.op.target, Clbit): - block_cargs.add(nd.op.target) - elif isinstance(nd.op.target, ClassicalRegister): - block_cargs.update(nd.op.target) - else: - block_cargs.update(node_resources(nd.op.target).clbits) - - block_qargs = [bit for bit in block_qargs if bit in wire_pos_map] - block_qargs.sort(key=wire_pos_map.get) - block_cargs = [bit for bit in block_cargs if bit in wire_pos_map] - block_cargs.sort(key=wire_pos_map.get) - new_node = DAGOpNode(op, block_qargs, block_cargs, dag=self) - - # check the op to insert matches the number of qubits we put it on - if op.num_qubits != len(block_qargs): - raise DAGCircuitError( - f"Number of qubits in the replacement operation ({op.num_qubits}) is not equal to " - f"the number of qubits in the block ({len(block_qargs)})!" - ) - - try: - new_node._node_id = self._multi_graph.contract_nodes( - block_ids, new_node, check_cycle=cycle_check - ) - except rx.DAGWouldCycle as ex: - raise DAGCircuitError( - "Replacing the specified node block would introduce a cycle" - ) from ex - - self._increment_op(op.name) - - for nd in node_block: - self._decrement_op(nd.name) - - return new_node - - def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condition=True): - """Replace one node with dag. - - Args: - node (DAGOpNode): node to substitute - input_dag (DAGCircuit): circuit that will substitute the node. - wires (list[Bit] | Dict[Bit, Bit]): gives an order for (qu)bits - in the input circuit. If a list, then the bits refer to those in the ``input_dag``, - and the order gets matched to the node wires by qargs first, then cargs, then - conditions. If a dictionary, then a mapping of bits in the ``input_dag`` to those - that the ``node`` acts on. - - Standalone :class:`~.expr.Var` nodes cannot currently be remapped as part of the - substitution; the ``input_dag`` should be defined over the correct set of variables - already. - - .. - The rule about not remapping `Var`s is to avoid performance pitfalls and reduce - complexity; the creator of the input DAG should easily be able to arrange for - the correct `Var`s to be used, and doing so avoids us needing to recurse through - control-flow operations to do deep remappings. - propagate_condition (bool): If ``True`` (default), then any ``condition`` attribute on - the operation within ``node`` is propagated to each node in the ``input_dag``. If - ``False``, then the ``input_dag`` is assumed to faithfully implement suitable - conditional logic already. This is ignored for :class:`.ControlFlowOp`\\ s (i.e. - treated as if it is ``False``); replacements of those must already fulfill the same - conditional logic or this function would be close to useless for them. - - Returns: - dict: maps node IDs from `input_dag` to their new node incarnations in `self`. - - Raises: - DAGCircuitError: if met with unexpected predecessor/successors - """ - if not isinstance(node, DAGOpNode): - raise DAGCircuitError(f"expected node DAGOpNode, got {type(node)}") - - if isinstance(wires, dict): - wire_map = wires - else: - wires = input_dag.wires if wires is None else wires - node_cargs = set(node.cargs) - node_wire_order = list(node.qargs) + list(node.cargs) - # If we're not propagating it, the number of wires in the input DAG should include the - # condition as well. - if not propagate_condition and _may_have_additional_wires(node): - node_wire_order += [ - wire for wire in _additional_wires(node.op) if wire not in node_cargs - ] - if len(wires) != len(node_wire_order): - raise DAGCircuitError( - f"bit mapping invalid: expected {len(node_wire_order)}, got {len(wires)}" - ) - wire_map = dict(zip(wires, node_wire_order)) - if len(wire_map) != len(node_wire_order): - raise DAGCircuitError("bit mapping invalid: some bits have duplicate entries") - for input_dag_wire, our_wire in wire_map.items(): - if our_wire not in self.input_map: - raise DAGCircuitError(f"bit mapping invalid: {our_wire} is not in this DAG") - if isinstance(our_wire, expr.Var) or isinstance(input_dag_wire, expr.Var): - raise DAGCircuitError("`Var` nodes cannot be remapped during substitution") - # Support mapping indiscriminately between Qubit and AncillaQubit, etc. - check_type = Qubit if isinstance(our_wire, Qubit) else Clbit - if not isinstance(input_dag_wire, check_type): - raise DAGCircuitError( - f"bit mapping invalid: {input_dag_wire} and {our_wire} are different bit types" - ) - if _may_have_additional_wires(node): - node_vars = {var for var in _additional_wires(node.op) if isinstance(var, expr.Var)} - else: - node_vars = set() - dag_vars = set(input_dag.iter_vars()) - if dag_vars - node_vars: - raise DAGCircuitError( - "Cannot replace a node with a DAG with more variables." - f" Variables in node: {node_vars}." - f" Variables in DAG: {dag_vars}." - ) - for var in dag_vars: - wire_map[var] = var - - reverse_wire_map = {b: a for a, b in wire_map.items()} - # It doesn't make sense to try and propagate a condition from a control-flow op; a - # replacement for the control-flow op should implement the operation completely. - if propagate_condition and not node.is_control_flow() and node.condition is not None: - in_dag = input_dag.copy_empty_like() - # The remapping of `condition` below is still using the old code that assumes a 2-tuple. - # This is because this remapping code only makes sense in the case of non-control-flow - # operations being replaced. These can only have the 2-tuple conditions, and the - # ability to set a condition at an individual node level will be deprecated and removed - # in favour of the new-style conditional blocks. The extra logic in here to add - # additional wires into the map as necessary would hugely complicate matters if we tried - # to abstract it out into the `VariableMapper` used elsewhere. - target, value = node.condition - if isinstance(target, Clbit): - new_target = reverse_wire_map.get(target, Clbit()) - if new_target not in wire_map: - in_dag.add_clbits([new_target]) - wire_map[new_target], reverse_wire_map[target] = target, new_target - target_cargs = {new_target} - else: # ClassicalRegister - mapped_bits = [reverse_wire_map.get(bit, Clbit()) for bit in target] - for ours, theirs in zip(target, mapped_bits): - # Update to any new dummy bits we just created to the wire maps. - wire_map[theirs], reverse_wire_map[ours] = ours, theirs - new_target = ClassicalRegister(bits=mapped_bits) - in_dag.add_creg(new_target) - target_cargs = set(new_target) - new_condition = (new_target, value) - for in_node in input_dag.topological_op_nodes(): - if getattr(in_node.op, "condition", None) is not None: - raise DAGCircuitError( - "cannot propagate a condition to an element that already has one" - ) - if target_cargs.intersection(in_node.cargs): - # This is for backwards compatibility with early versions of the method, as it is - # a tested part of the API. In the newer model of a condition being an integral - # part of the operation (not a separate property to be copied over), this error - # is overzealous, because it forbids a custom instruction from implementing the - # condition within its definition rather than at the top level. - raise DAGCircuitError( - "cannot propagate a condition to an element that acts on those bits" - ) - new_op = copy.copy(in_node.op) - if new_condition: - if not isinstance(new_op, ControlFlowOp): - new_op = new_op.c_if(*new_condition) - else: - new_op.condition = new_condition - in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs, check=False) - else: - in_dag = input_dag - - if in_dag.global_phase: - self.global_phase += in_dag.global_phase - - # Add wire from pred to succ if no ops on mapped wire on ``in_dag`` - # rustworkx's substitute_node_with_subgraph lacks the DAGCircuit - # context to know what to do in this case (the method won't even see - # these nodes because they're filtered) so we manually retain the - # edges prior to calling substitute_node_with_subgraph and set the - # edge_map_fn callback kwarg to skip these edges when they're - # encountered. - for in_dag_wire, self_wire in wire_map.items(): - input_node = in_dag.input_map[in_dag_wire] - output_node = in_dag.output_map[in_dag_wire] - if in_dag._multi_graph.has_edge(input_node._node_id, output_node._node_id): - pred = self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge, wire=self_wire: edge == wire - )[0] - succ = self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge, wire=self_wire: edge == wire - )[0] - self._multi_graph.add_edge(pred._node_id, succ._node_id, self_wire) - for contracted_var in node_vars - dag_vars: - pred = self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge, wire=contracted_var: edge == wire - )[0] - succ = self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge, wire=contracted_var: edge == wire - )[0] - self._multi_graph.add_edge(pred._node_id, succ._node_id, contracted_var) - - # Exclude any nodes from in_dag that are not a DAGOpNode or are on - # wires outside the set specified by the wires kwarg - def filter_fn(node): - if not isinstance(node, DAGOpNode): - return False - for _, _, wire in in_dag.edges(node): - if wire not in wire_map: - return False - return True - - # Map edges into and out of node to the appropriate node from in_dag - def edge_map_fn(source, _target, self_wire): - wire = reverse_wire_map[self_wire] - # successor edge - if source == node._node_id: - wire_output_id = in_dag.output_map[wire]._node_id - out_index = in_dag._multi_graph.predecessor_indices(wire_output_id)[0] - # Edge directly from from input nodes to output nodes in in_dag are - # already handled prior to calling rustworkx. Don't map these edges - # in rustworkx. - if not isinstance(in_dag._multi_graph[out_index], DAGOpNode): - return None - # predecessor edge - else: - wire_input_id = in_dag.input_map[wire]._node_id - out_index = in_dag._multi_graph.successor_indices(wire_input_id)[0] - # Edge directly from from input nodes to output nodes in in_dag are - # already handled prior to calling rustworkx. Don't map these edges - # in rustworkx. - if not isinstance(in_dag._multi_graph[out_index], DAGOpNode): - return None - return out_index - - # Adjust edge weights from in_dag - def edge_weight_map(wire): - return wire_map[wire] - - node_map = self._multi_graph.substitute_node_with_subgraph( - node._node_id, in_dag._multi_graph, edge_map_fn, filter_fn, edge_weight_map - ) - self._decrement_op(node.name) - - variable_mapper = _classical_resource_map.VariableMapper( - self.cregs.values(), wire_map, add_register=self.add_creg - ) - # Iterate over nodes of input_circuit and update wires in node objects migrated - # from in_dag - for old_node_index, new_node_index in node_map.items(): - # update node attributes - old_node = in_dag._multi_graph[old_node_index] - m_op = None - if not old_node.is_standard_gate() and isinstance(old_node.op, SwitchCaseOp): - m_op = SwitchCaseOp( - variable_mapper.map_target(old_node.op.target), - old_node.op.cases_specifier(), - label=old_node.op.label, - ) - elif old_node.condition is not None: - m_op = old_node.op - if old_node.is_control_flow(): - m_op.condition = variable_mapper.map_condition(m_op.condition) - else: - new_condition = variable_mapper.map_condition(m_op.condition) - if new_condition is not None: - m_op = m_op.c_if(*new_condition) - m_qargs = [wire_map[x] for x in old_node.qargs] - m_cargs = [wire_map[x] for x in old_node.cargs] - old_instruction = old_node._to_circuit_instruction() - if m_op is None: - new_instruction = old_instruction.replace(qubits=m_qargs, clbits=m_cargs) - else: - new_instruction = old_instruction.replace( - operation=m_op, qubits=m_qargs, clbits=m_cargs - ) - new_node = DAGOpNode.from_instruction(new_instruction) - new_node._node_id = new_node_index - self._multi_graph[new_node_index] = new_node - self._increment_op(new_node.name) - - return {k: self._multi_graph[v] for k, v in node_map.items()} - - def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_condition=True): - """Replace an DAGOpNode with a single operation. qargs, cargs and - conditions for the new operation will be inferred from the node to be - replaced. The new operation will be checked to match the shape of the - replaced operation. - - Args: - node (DAGOpNode): Node to be replaced - op (qiskit.circuit.Operation): The :class:`qiskit.circuit.Operation` - instance to be added to the DAG - inplace (bool): Optional, default False. If True, existing DAG node - will be modified to include op. Otherwise, a new DAG node will - be used. - propagate_condition (bool): Optional, default True. If True, a condition on the - ``node`` to be replaced will be applied to the new ``op``. This is the legacy - behavior. If either node is a control-flow operation, this will be ignored. If - the ``op`` already has a condition, :exc:`.DAGCircuitError` is raised. - - Returns: - DAGOpNode: the new node containing the added operation. - - Raises: - DAGCircuitError: If replacement operation was incompatible with - location of target node. - """ - - if not isinstance(node, DAGOpNode): - raise DAGCircuitError("Only DAGOpNodes can be replaced.") - - if node.op.num_qubits != op.num_qubits or node.op.num_clbits != op.num_clbits: - raise DAGCircuitError( - f"Cannot replace node of width ({node.op.num_qubits} qubits, " - f"{node.op.num_clbits} clbits) with " - f"operation of mismatched width ({op.num_qubits} qubits, " - f"{op.num_clbits} clbits)." - ) - - # This might include wires that are inherent to the node, like in its `condition` or - # `target` fields, so might be wider than `node.op.num_{qu,cl}bits`. - current_wires = {wire for _, _, wire in self.edges(node)} - new_wires = set(node.qargs) | set(node.cargs) | set(_additional_wires(op)) - - if propagate_condition and not ( - isinstance(node.op, ControlFlowOp) or isinstance(op, ControlFlowOp) - ): - if getattr(op, "condition", None) is not None: - raise DAGCircuitError( - "Cannot propagate a condition to an operation that already has one." - ) - if (old_condition := getattr(node.op, "condition", None)) is not None: - if not isinstance(op, Instruction): - raise DAGCircuitError("Cannot add a condition on a generic Operation.") - if not isinstance(node.op, ControlFlowOp): - op = op.c_if(*old_condition) - else: - op.condition = old_condition - new_wires.update(condition_resources(old_condition).clbits) - - if new_wires != current_wires: - # The new wires must be a non-strict subset of the current wires; if they add new wires, - # we'd not know where to cut the existing wire to insert the new dependency. - raise DAGCircuitError( - f"New operation '{op}' does not span the same wires as the old node '{node}'." - f" New wires: {new_wires}, old wires: {current_wires}." - ) - - if inplace: - if op.name != node.op.name: - self._increment_op(op.name) - self._decrement_op(node.name) - node.op = op - return node - - new_node = copy.copy(node) - new_node.op = op - self._multi_graph[node._node_id] = new_node - if op.name != node.name: - self._increment_op(op.name) - self._decrement_op(node.name) - return new_node - - def separable_circuits( - self, remove_idle_qubits: bool = False, *, vars_mode: _VarsMode = "alike" - ) -> list["DAGCircuit"]: - """Decompose the circuit into sets of qubits with no gates connecting them. - - Args: - remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from - the separated circuits. If ``False``, each output circuit will contain the - same number of qubits as ``self``. - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - - Returns: - List[DAGCircuit]: The circuits resulting from separating ``self`` into sets - of disconnected qubits - - Each :class:`~.DAGCircuit` instance returned by this method will contain the same number of - clbits as ``self``. The global phase information in ``self`` will not be maintained - in the subcircuits returned by this method. - """ - connected_components = rx.weakly_connected_components(self._multi_graph) - - # Collect each disconnected subgraph - disconnected_subgraphs = [] - for components in connected_components: - disconnected_subgraphs.append(self._multi_graph.subgraph(list(components))) - - # Helper function for ensuring rustworkx nodes are returned in lexicographical, - # topological order - def _key(x): - return x.sort_key - - # Create new DAGCircuit objects from each of the rustworkx subgraph objects - decomposed_dags = [] - for subgraph in disconnected_subgraphs: - new_dag = self.copy_empty_like(vars_mode=vars_mode) - new_dag.global_phase = 0 - subgraph_is_classical = True - for node in rx.lexicographical_topological_sort(subgraph, key=_key): - if isinstance(node, DAGInNode): - if isinstance(node.wire, Qubit): - subgraph_is_classical = False - if not isinstance(node, DAGOpNode): - continue - new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - - # Ignore DAGs created for empty clbits - if not subgraph_is_classical: - decomposed_dags.append(new_dag) - - if remove_idle_qubits: - for dag in decomposed_dags: - dag.remove_qubits(*(bit for bit in dag.idle_wires() if isinstance(bit, Qubit))) - - return decomposed_dags - - def swap_nodes(self, node1, node2): - """Swap connected nodes e.g. due to commutation. - - Args: - node1 (OpNode): predecessor node - node2 (OpNode): successor node - - Raises: - DAGCircuitError: if either node is not an OpNode or nodes are not connected - """ - if not (isinstance(node1, DAGOpNode) and isinstance(node2, DAGOpNode)): - raise DAGCircuitError("nodes to swap are not both DAGOpNodes") - try: - connected_edges = self._multi_graph.get_all_edge_data(node1._node_id, node2._node_id) - except rx.NoEdgeBetweenNodes as no_common_edge: - raise DAGCircuitError("attempt to swap unconnected nodes") from no_common_edge - node1_id = node1._node_id - node2_id = node2._node_id - for edge in connected_edges[::-1]: - edge_find = lambda x, y=edge: x == y - edge_parent = self._multi_graph.find_predecessors_by_edge(node1_id, edge_find)[0] - self._multi_graph.remove_edge(edge_parent._node_id, node1_id) - self._multi_graph.add_edge(edge_parent._node_id, node2_id, edge) - edge_child = self._multi_graph.find_successors_by_edge(node2_id, edge_find)[0] - self._multi_graph.remove_edge(node1_id, node2_id) - self._multi_graph.add_edge(node2_id, node1_id, edge) - self._multi_graph.remove_edge(node2_id, edge_child._node_id) - self._multi_graph.add_edge(node1_id, edge_child._node_id, edge) - - def node(self, node_id): - """Get the node in the dag. - - Args: - node_id(int): Node identifier. - - Returns: - node: the node. - """ - return self._multi_graph[node_id] - - def nodes(self): - """Iterator for node values. - - Yield: - node: the node. - """ - yield from self._multi_graph.nodes() - - def edges(self, nodes=None): - """Iterator for edge values and source and dest node - - This works by returning the output edges from the specified nodes. If - no nodes are specified all edges from the graph are returned. - - Args: - nodes(DAGOpNode, DAGInNode, or DAGOutNode|list(DAGOpNode, DAGInNode, or DAGOutNode): - Either a list of nodes or a single input node. If none is specified, - all edges are returned from the graph. - - Yield: - edge: the edge in the same format as out_edges the tuple - (source node, destination node, edge data) - """ - if nodes is None: - nodes = self._multi_graph.nodes() - - elif isinstance(nodes, (DAGOpNode, DAGInNode, DAGOutNode)): - nodes = [nodes] - for node in nodes: - raw_nodes = self._multi_graph.out_edges(node._node_id) - for source, dest, edge in raw_nodes: - yield (self._multi_graph[source], self._multi_graph[dest], edge) - - def op_nodes(self, op=None, include_directives=True): - """Get the list of "op" nodes in the dag. - - Args: - op (Type): :class:`qiskit.circuit.Operation` subclass op nodes to - return. If None, return all op nodes. - include_directives (bool): include `barrier`, `snapshot` etc. - - Returns: - list[DAGOpNode]: the list of node ids containing the given op. - """ - nodes = [] - filter_is_nonstandard = getattr(op, "_standard_gate", None) is None - for node in self._multi_graph.nodes(): - if isinstance(node, DAGOpNode): - if not include_directives and node.is_directive(): - continue - if op is None or ( - # This middle catch is to avoid Python-space operation creation for most uses of - # `op`; we're usually just looking for control-flow ops, and standard gates - # aren't control-flow ops. - not (filter_is_nonstandard and node.is_standard_gate()) - and isinstance(node.op, op) - ): - nodes.append(node) - return nodes - - def gate_nodes(self): - """Get the list of gate nodes in the dag. - - Returns: - list[DAGOpNode]: the list of DAGOpNodes that represent gates. - """ - nodes = [] - for node in self.op_nodes(): - if isinstance(node.op, Gate): - nodes.append(node) - return nodes - - def named_nodes(self, *names): - """Get the set of "op" nodes with the given name.""" - named_nodes = [] - for node in self._multi_graph.nodes(): - if isinstance(node, DAGOpNode) and node.name in names: - named_nodes.append(node) - return named_nodes - - def two_qubit_ops(self): - """Get list of 2 qubit operations. Ignore directives like snapshot and barrier.""" - ops = [] - for node in self.op_nodes(include_directives=False): - if len(node.qargs) == 2: - ops.append(node) - return ops - - def multi_qubit_ops(self): - """Get list of 3+ qubit operations. Ignore directives like snapshot and barrier.""" - ops = [] - for node in self.op_nodes(include_directives=False): - if len(node.qargs) >= 3: - ops.append(node) - return ops - - def longest_path(self): - """Returns the longest path in the dag as a list of DAGOpNodes, DAGInNodes, and DAGOutNodes.""" - return [self._multi_graph[x] for x in rx.dag_longest_path(self._multi_graph)] - - def successors(self, node): - """Returns iterator of the successors of a node as DAGOpNodes and DAGOutNodes.""" - return iter(self._multi_graph.successors(node._node_id)) - - def predecessors(self, node): - """Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes.""" - return iter(self._multi_graph.predecessors(node._node_id)) - - def op_successors(self, node): - """Returns iterator of "op" successors of a node in the dag.""" - return (succ for succ in self.successors(node) if isinstance(succ, DAGOpNode)) - - def op_predecessors(self, node): - """Returns the iterator of "op" predecessors of a node in the dag.""" - return (pred for pred in self.predecessors(node) if isinstance(pred, DAGOpNode)) - - def is_successor(self, node, node_succ): - """Checks if a second node is in the successors of node.""" - return self._multi_graph.has_edge(node._node_id, node_succ._node_id) - - def is_predecessor(self, node, node_pred): - """Checks if a second node is in the predecessors of node.""" - return self._multi_graph.has_edge(node_pred._node_id, node._node_id) - - def quantum_predecessors(self, node): - """Returns iterator of the predecessors of a node that are - connected by a quantum edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Qubit) - ) - ) - - def classical_predecessors(self, node): - """Returns iterator of the predecessors of a node that are - connected by a classical edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) - ) - ) - - def ancestors(self, node): - """Returns set of the ancestors of a node as DAGOpNodes and DAGInNodes.""" - return {self._multi_graph[x] for x in rx.ancestors(self._multi_graph, node._node_id)} - - def descendants(self, node): - """Returns set of the descendants of a node as DAGOpNodes and DAGOutNodes.""" - return {self._multi_graph[x] for x in rx.descendants(self._multi_graph, node._node_id)} - - def bfs_successors(self, node): - """ - Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node - and [DAGNode] is its successors in BFS order. - """ - return iter(rx.bfs_successors(self._multi_graph, node._node_id)) - - def quantum_successors(self, node): - """Returns iterator of the successors of a node that are - connected by a quantum edge as Opnodes and DAGOutNodes.""" - return iter( - self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Qubit) - ) - ) - - def classical_successors(self, node): - """Returns iterator of the successors of a node that are - connected by a classical edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) - ) - ) - - def remove_op_node(self, node): - """Remove an operation node n. - - Add edges from predecessors to successors. - """ - if not isinstance(node, DAGOpNode): - raise DAGCircuitError( - f'The method remove_op_node only works on DAGOpNodes. A "{type(node)}" ' - "node type was wrongly provided." - ) - - self._multi_graph.remove_node_retain_edges_by_id(node._node_id) - self._decrement_op(node.name) - - def remove_ancestors_of(self, node): - """Remove all of the ancestor operation nodes of node.""" - anc = rx.ancestors(self._multi_graph, node) - # TODO: probably better to do all at once using - # multi_graph.remove_nodes_from; same for related functions ... - - for anc_node in anc: - if isinstance(anc_node, DAGOpNode): - self.remove_op_node(anc_node) - - def remove_descendants_of(self, node): - """Remove all of the descendant operation nodes of node.""" - desc = rx.descendants(self._multi_graph, node) - for desc_node in desc: - if isinstance(desc_node, DAGOpNode): - self.remove_op_node(desc_node) - - def remove_nonancestors_of(self, node): - """Remove all of the non-ancestors operation nodes of node.""" - anc = rx.ancestors(self._multi_graph, node) - comp = list(set(self._multi_graph.nodes()) - set(anc)) - for n in comp: - if isinstance(n, DAGOpNode): - self.remove_op_node(n) - - def remove_nondescendants_of(self, node): - """Remove all of the non-descendants operation nodes of node.""" - dec = rx.descendants(self._multi_graph, node) - comp = list(set(self._multi_graph.nodes()) - set(dec)) - for n in comp: - if isinstance(n, DAGOpNode): - self.remove_op_node(n) - - def front_layer(self): - """Return a list of op nodes in the first layer of this dag.""" - graph_layers = self.multigraph_layers() - try: - next(graph_layers) # Remove input nodes - except StopIteration: - return [] - - op_nodes = [node for node in next(graph_layers) if isinstance(node, DAGOpNode)] - - return op_nodes - - def layers(self, *, vars_mode: _VarsMode = "captures"): - """Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit. - - A layer is a circuit whose gates act on disjoint qubits, i.e., - a layer has depth 1. The total number of layers equals the - circuit depth d. The layers are indexed from 0 to d-1 with the - earliest layer at index 0. The layers are constructed using a - greedy algorithm. Each returned layer is a dict containing - {"graph": circuit graph, "partition": list of qubit lists}. - - The returned layer contains new (but semantically equivalent) DAGOpNodes, DAGInNodes, - and DAGOutNodes. These are not the same as nodes of the original dag, but are equivalent - via DAGNode.semantic_eq(node1, node2). - - TODO: Gates that use the same cbits will end up in different - layers as this is currently implemented. This may not be - the desired behavior. - - Args: - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - """ - graph_layers = self.multigraph_layers() - try: - next(graph_layers) # Remove input nodes - except StopIteration: - return - - for graph_layer in graph_layers: - - # Get the op nodes from the layer, removing any input and output nodes. - op_nodes = [node for node in graph_layer if isinstance(node, DAGOpNode)] - - # Sort to make sure they are in the order they were added to the original DAG - # It has to be done by node_id as graph_layer is just a list of nodes - # with no implied topology - # Drawing tools rely on _node_id to infer order of node creation - # so we need this to be preserved by layers() - op_nodes.sort(key=lambda nd: nd._node_id) - - # Stop yielding once there are no more op_nodes in a layer. - if not op_nodes: - return - - # Construct a shallow copy of self - new_layer = self.copy_empty_like(vars_mode=vars_mode) - - for node in op_nodes: - new_layer._apply_op_node_back(node, check=False) - - # The quantum registers that have an operation in this layer. - support_list = [ - op_node.qargs for op_node in new_layer.op_nodes() if not op_node.is_directive() - ] - - yield {"graph": new_layer, "partition": support_list} - - def serial_layers(self, *, vars_mode: _VarsMode = "captures"): - """Yield a layer for all gates of this circuit. - - A serial layer is a circuit with one gate. The layers have the - same structure as in layers(). - - Args: - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - """ - for next_node in self.topological_op_nodes(): - new_layer = self.copy_empty_like(vars_mode=vars_mode) - - # Save the support of the operation we add to the layer - support_list = [] - # Operation data - op = copy.copy(next_node.op) - qargs = copy.copy(next_node.qargs) - cargs = copy.copy(next_node.cargs) - - # Add node to new_layer - new_layer.apply_operation_back(op, qargs, cargs, check=False) - # Add operation to partition - if not getattr(next_node.op, "_directive", False): - support_list.append(list(qargs)) - l_dict = {"graph": new_layer, "partition": support_list} - yield l_dict - - def multigraph_layers(self): - """Yield layers of the multigraph.""" - first_layer = [x._node_id for x in self.input_map.values()] - return iter(rx.layers(self._multi_graph, first_layer)) - - def collect_runs(self, namelist): - """Return a set of non-conditional runs of "op" nodes with the given names. - - For example, "... h q[0]; cx q[0],q[1]; cx q[0],q[1]; h q[1]; .." - would produce the tuple of cx nodes as an element of the set returned - from a call to collect_runs(["cx"]). If instead the cx nodes were - "cx q[0],q[1]; cx q[1],q[0];", the method would still return the - pair in a tuple. The namelist can contain names that are not - in the circuit's basis. - - Nodes must have only one successor to continue the run. - """ - - def filter_fn(node): - return isinstance(node, DAGOpNode) and node.name in namelist and node.condition is None - - group_list = rx.collect_runs(self._multi_graph, filter_fn) - return {tuple(x) for x in group_list} - - def collect_1q_runs(self) -> list[list[DAGOpNode]]: - """Return a set of non-conditional runs of 1q "op" nodes.""" - return rx.collect_runs(self._multi_graph, collect_1q_runs_filter) - - def collect_2q_runs(self): - """Return a set of non-conditional runs of 2q "op" nodes.""" - - def color_fn(edge): - if isinstance(edge, Qubit): - return self.find_bit(edge).index - else: - return None - - return rx.collect_bicolor_runs(self._multi_graph, collect_2q_blocks_filter, color_fn) - - def nodes_on_wire(self, wire, only_ops=False): - """ - Iterator for nodes that affect a given wire. - - Args: - wire (Bit): the wire to be looked at. - only_ops (bool): True if only the ops nodes are wanted; - otherwise, all nodes are returned. - Yield: - Iterator: the successive nodes on the given wire - - Raises: - DAGCircuitError: if the given wire doesn't exist in the DAG - """ - current_node = self.input_map.get(wire, None) - - if not current_node: - raise DAGCircuitError(f"The given wire {str(wire)} is not present in the circuit") - - more_nodes = True - while more_nodes: - more_nodes = False - # allow user to just get ops on the wire - not the input/output nodes - if isinstance(current_node, DAGOpNode) or not only_ops: - yield current_node - - try: - current_node = self._multi_graph.find_adjacent_node_by_edge( - current_node._node_id, lambda x: wire == x - ) - more_nodes = True - except rx.NoSuitableNeighbors: - pass - - def count_ops(self, *, recurse: bool = True): - """Count the occurrences of operation names. - - Args: - recurse: if ``True`` (default), then recurse into control-flow operations. In all - cases, this counts only the number of times the operation appears in any possible - block; both branches of if-elses are counted, and for- and while-loop blocks are - only counted once. - - Returns: - Mapping[str, int]: a mapping of operation names to the number of times it appears. - """ - if not recurse or not CONTROL_FLOW_OP_NAMES.intersection(self._op_names): - return self._op_names.copy() - - # pylint: disable=cyclic-import - from qiskit.converters import circuit_to_dag - - def inner(dag, counts): - for name, count in dag._op_names.items(): - counts[name] += count - for node in dag.op_nodes(ControlFlowOp): - for block in node.op.blocks: - counts = inner(circuit_to_dag(block), counts) - return counts - - return dict(inner(self, defaultdict(int))) - - def count_ops_longest_path(self): - """Count the occurrences of operation names on the longest path. - - Returns a dictionary of counts keyed on the operation name. - """ - op_dict = {} - path = self.longest_path() - path = path[1:-1] # remove qubits at beginning and end of path - for node in path: - name = node.op.name - if name not in op_dict: - op_dict[name] = 1 - else: - op_dict[name] += 1 - return op_dict - - def quantum_causal_cone(self, qubit): - """ - Returns causal cone of a qubit. - - A qubit's causal cone is the set of qubits that can influence the output of that - qubit through interactions, whether through multi-qubit gates or operations. Knowing - the causal cone of a qubit can be useful when debugging faulty circuits, as it can - help identify which wire(s) may be causing the problem. - - This method does not consider any classical data dependency in the ``DAGCircuit``, - classical bit wires are ignored for the purposes of building the causal cone. - - Args: - qubit (~qiskit.circuit.Qubit): The output qubit for which we want to find the causal cone. - - Returns: - Set[~qiskit.circuit.Qubit]: The set of qubits whose interactions affect ``qubit``. - """ - # Retrieve the output node from the qubit - output_node = self.output_map.get(qubit, None) - if not output_node: - raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.") - - qubits_in_cone = {qubit} - queue = deque(self.quantum_predecessors(output_node)) - - # The processed_non_directive_nodes stores the set of processed non-directive nodes. - # This is an optimization to avoid considering the same non-directive node multiple - # times when reached from different paths. - # The directive nodes (such as barriers or measures) are trickier since when processing - # them we only add their predecessors that intersect qubits_in_cone. Hence, directive - # nodes have to be considered multiple times. - processed_non_directive_nodes = set() - - while queue: - node_to_check = queue.popleft() - - if isinstance(node_to_check, DAGOpNode): - # If the operation is not a directive (in particular not a barrier nor a measure), - # we do not do anything if it was already processed. Otherwise, we add its qubits - # to qubits_in_cone, and append its predecessors to queue. - if not getattr(node_to_check.op, "_directive"): - if node_to_check in processed_non_directive_nodes: - continue - qubits_in_cone = qubits_in_cone.union(set(node_to_check.qargs)) - processed_non_directive_nodes.add(node_to_check) - for pred in self.quantum_predecessors(node_to_check): - if isinstance(pred, DAGOpNode): - queue.append(pred) - else: - # Directives (such as barriers and measures) may be defined over all the qubits, - # yet not all of these qubits should be considered in the causal cone. So we - # only add those predecessors that have qubits in common with qubits_in_cone. - for pred in self.quantum_predecessors(node_to_check): - if isinstance(pred, DAGOpNode) and not qubits_in_cone.isdisjoint( - set(pred.qargs) - ): - queue.append(pred) - - return qubits_in_cone - - def properties(self): - """Return a dictionary of circuit properties.""" - summary = { - "size": self.size(), - "depth": self.depth(), - "width": self.width(), - "qubits": self.num_qubits(), - "bits": self.num_clbits(), - "factors": self.num_tensor_factors(), - "operations": self.count_ops(), - } - return summary - - def draw(self, scale=0.7, filename=None, style="color"): - """ - Draws the dag circuit. - - This function needs `Graphviz `_ to be - installed. Graphviz is not a python package and can't be pip installed - (the ``graphviz`` package on PyPI is a Python interface library for - Graphviz and does not actually install Graphviz). You can refer to - `the Graphviz documentation `__ on - how to install it. - - Args: - scale (float): scaling factor - filename (str): file path to save image to (format inferred from name) - style (str): - 'plain': B&W graph; - 'color' (default): color input/output/op nodes - - Returns: - Ipython.display.Image: if in Jupyter notebook and not saving to file, - otherwise None. - """ - from qiskit.visualization.dag_visualization import dag_drawer - - return dag_drawer(dag=self, scale=scale, filename=filename, style=style) - - -class _DAGVarType(enum.Enum): - INPUT = enum.auto() - CAPTURE = enum.auto() - DECLARE = enum.auto() - - -class _DAGVarInfo: - __slots__ = ("var", "type", "in_node", "out_node") - - def __init__(self, var: expr.Var, type_: _DAGVarType, in_node: DAGInNode, out_node: DAGOutNode): - self.var = var - self.type = type_ - self.in_node = in_node - self.out_node = out_node - - -def _may_have_additional_wires(node) -> bool: - """Return whether a given :class:`.DAGOpNode` may contain references to additional wires - locations within its :class:`.Operation`. If this is ``True``, it doesn't necessarily mean - that the operation _will_ access memory inherently, but a ``False`` return guarantees that it - won't. - - The memory might be classical bits or classical variables, such as a control-flow operation or a - store. - - Args: - operation (qiskit.dagcircuit.DAGOpNode): the operation to check. - """ - # This is separate to `_additional_wires` because most of the time there won't be any extra - # wires beyond the explicit `qargs` and `cargs` so we want a fast path to be able to skip - # creating and testing a generator for emptiness. - # - # If updating this, you most likely also need to update `_additional_wires`. - return node.condition is not None or ( - not node.is_standard_gate() and isinstance(node.op, (ControlFlowOp, Store)) - ) - - -def _additional_wires(operation) -> Iterable[Clbit | expr.Var]: - """Return an iterable over the additional tracked memory usage in this operation. These - additional wires include (for example, non-exhaustive) bits referred to by a ``condition`` or - the classical variables involved in control-flow operations. - - Args: - operation: the :class:`~.circuit.Operation` instance for a node. - - Returns: - Iterable: the additional wires inherent to this operation. - """ - # If updating this, you likely need to update `_may_have_additional_wires` too. - if (condition := getattr(operation, "condition", None)) is not None: - if isinstance(condition, expr.Expr): - yield from _wires_from_expr(condition) - else: - yield from condition_resources(condition).clbits - if isinstance(operation, ControlFlowOp): - yield from operation.iter_captured_vars() - if isinstance(operation, SwitchCaseOp): - target = operation.target - if isinstance(target, Clbit): - yield target - elif isinstance(target, ClassicalRegister): - yield from target - else: - yield from _wires_from_expr(target) - elif isinstance(operation, Store): - yield from _wires_from_expr(operation.lvalue) - yield from _wires_from_expr(operation.rvalue) - - -def _wires_from_expr(node: expr.Expr) -> Iterable[Clbit | expr.Var]: - for var in expr.iter_vars(node): - if isinstance(var.var, Clbit): - yield var.var - elif isinstance(var.var, ClassicalRegister): - yield from var.var - else: - yield var +from qiskit._accelerate.circuit import DAGCircuit # pylint: disable=unused-import diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 9f35f6eda898..60e2a7465707 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -21,7 +21,6 @@ from qiskit.circuit import ( Clbit, ClassicalRegister, - ControlFlowOp, IfElseOp, WhileLoopOp, SwitchCaseOp, @@ -175,67 +174,3 @@ def _for_loop_eq(node1, node2, bit_indices1, bit_indices2): } _SEMANTIC_EQ_SYMMETRIC = frozenset({"barrier", "swap", "break_loop", "continue_loop"}) - - -# Note: called from dag_node.rs. -def _semantic_eq(node1, node2, bit_indices1, bit_indices2): - """ - Check if DAG nodes are considered equivalent, e.g., as a node_match for - :func:`rustworkx.is_isomorphic_node_match`. - - Args: - node1 (DAGOpNode, DAGInNode, DAGOutNode): A node to compare. - node2 (DAGOpNode, DAGInNode, DAGOutNode): The other node to compare. - bit_indices1 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node1 - bit_indices2 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node2 - - Return: - Bool: If node1 == node2 - """ - if not isinstance(node1, DAGOpNode) or not isinstance(node1, DAGOpNode): - return type(node1) is type(node2) and bit_indices1.get(node1.wire) == bit_indices2.get( - node2.wire - ) - if isinstance(node1.op, ControlFlowOp) and isinstance(node2.op, ControlFlowOp): - # While control-flow operations aren't represented natively in the DAG, we have to do - # some unpleasant dispatching and very manual handling. Once they have more first-class - # support we'll still be dispatching, but it'll look more appropriate (like the dispatch - # based on `DAGOpNode`/`DAGInNode`/`DAGOutNode` that already exists) and less like we're - # duplicating code from the `ControlFlowOp` classes. - if type(node1.op) is not type(node2.op): - return False - comparer = _SEMANTIC_EQ_CONTROL_FLOW.get(type(node1.op)) - if comparer is None: # pragma: no cover - raise RuntimeError(f"unhandled control-flow operation: {type(node1.op)}") - return comparer(node1, node2, bit_indices1, bit_indices2) - - node1_qargs = [bit_indices1[qarg] for qarg in node1.qargs] - node1_cargs = [bit_indices1[carg] for carg in node1.cargs] - - node2_qargs = [bit_indices2[qarg] for qarg in node2.qargs] - node2_cargs = [bit_indices2[carg] for carg in node2.cargs] - - # For barriers, qarg order is not significant so compare as sets - if node1.op.name == node2.op.name and node1.name in _SEMANTIC_EQ_SYMMETRIC: - node1_qargs = set(node1_qargs) - node1_cargs = set(node1_cargs) - node2_qargs = set(node2_qargs) - node2_cargs = set(node2_cargs) - - return ( - node1_qargs == node2_qargs - and node1_cargs == node2_cargs - and _legacy_condition_eq( - getattr(node1.op, "condition", None), - getattr(node2.op, "condition", None), - bit_indices1, - bit_indices2, - ) - and node1.op == node2.op - ) - - -# Bind semantic_eq from Python to Rust implementation -DAGNode.semantic_eq = staticmethod(_semantic_eq) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 86c5cba8295f..633de3a64f78 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -639,7 +639,8 @@ def __call__( """ if use_dag: - from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode + from qiskit.dagcircuit.dagcircuit import DAGCircuit + from qiskit.dagcircuit.dagnode import DAGOpNode sequence = self._inner_decomposer( np.asarray(unitary, dtype=complex), @@ -659,7 +660,7 @@ def __call__( op = CircuitInstruction.from_standard( gate, qubits=tuple(q[x] for x in qubits), params=params ) - node = DAGOpNode.from_instruction(op, dag=dag) + node = DAGOpNode.from_instruction(op) dag._apply_op_node_back(node) return dag else: diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index a5a3936b1d7e..5737385af15b 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -313,10 +313,7 @@ def _replace_node(self, dag, node, instr_map): if node.params: parameter_map = dict(zip(target_params, node.params)) for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction( - inner_node._to_circuit_instruction(), - dag=target_dag, - ) + new_node = DAGOpNode.from_instruction(inner_node._to_circuit_instruction()) new_node.qargs = tuple( node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs ) @@ -366,7 +363,6 @@ def _replace_node(self, dag, node, instr_map): for inner_node in target_dag.topological_op_nodes(): new_node = DAGOpNode.from_instruction( inner_node._to_circuit_instruction(), - dag=target_dag, ) new_node.qargs = tuple( node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs diff --git a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py index 73e1d4ac5484..0c6d780f052a 100644 --- a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py +++ b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py @@ -58,7 +58,9 @@ def run(self, dag): continue if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) continue if self.target is not None: diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 99bf95147ae2..51e116033bb6 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -66,7 +66,9 @@ def run(self, dag): for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) continue if getattr(node.op, "_directive", False): diff --git a/qiskit/transpiler/passes/calibration/rzx_templates.py b/qiskit/transpiler/passes/calibration/rzx_templates.py index 406e5e75de04..10f4d19ebd9e 100644 --- a/qiskit/transpiler/passes/calibration/rzx_templates.py +++ b/qiskit/transpiler/passes/calibration/rzx_templates.py @@ -20,17 +20,6 @@ from qiskit.circuit.library.templates import rzx -class RZXTemplateMap(Enum): - """Mapping of instruction name to decomposition template.""" - - ZZ1 = rzx.rzx_zz1() - ZZ2 = rzx.rzx_zz2() - ZZ3 = rzx.rzx_zz3() - YZ = rzx.rzx_yz() - XZ = rzx.rzx_xz() - CY = rzx.rzx_cy() - - def rzx_templates(template_list: List[str] = None) -> Dict: """Convenience function to get the cost_dict and templates for template matching. @@ -40,6 +29,17 @@ def rzx_templates(template_list: List[str] = None) -> Dict: Returns: Decomposition templates and cost values. """ + + class RZXTemplateMap(Enum): + """Mapping of instruction name to decomposition template.""" + + ZZ1 = rzx.rzx_zz1() + ZZ2 = rzx.rzx_zz2() + ZZ3 = rzx.rzx_zz3() + YZ = rzx.rzx_yz() + XZ = rzx.rzx_xz() + CY = rzx.rzx_cy() + if template_list is None: template_list = ["zz1", "zz2", "zz3", "yz", "xz", "cy"] diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index 629dc32061fc..7514ac6b421f 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -118,6 +118,6 @@ def run(self, dag): } out_layout = Layout(final_layout_mapping) self.property_set["final_layout"] = out_layout - new_dag._global_phase = dag._global_phase + new_dag.global_phase = dag.global_phase return new_dag diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 4ce94ecdb62f..7e7031ec3361 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -310,7 +310,7 @@ def run(self, dag): mapped_dag.add_captured_var(var) for var in dag.iter_declared_vars(): mapped_dag.add_declared_var(var) - mapped_dag._global_phase = dag._global_phase + mapped_dag.global_phase = dag.global_phase self.property_set["original_qubit_indices"] = { bit: index for index, bit in enumerate(dag.qubits) } diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index adfc4d73a221..836fa112fd84 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -219,5 +219,7 @@ def _handle_control_flow_ops(self, dag): for block in node.op.blocks: new_circ = pass_manager.run(block) mapped_blocks.append(new_circ) - node.op = node.op.replace_blocks(mapped_blocks) + dag.substitute_node( + node, node.op.replace_blocks(mapped_blocks), propagate_condition=False + ) return dag diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 12f7285af9dc..49f227e8a746 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -202,7 +202,11 @@ def _handle_control_flow_ops(self, dag): for node in dag.op_nodes(): if node.name not in CONTROL_FLOW_OP_NAMES: continue - node.op = node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks) + dag.substitute_node( + node, + node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks), + propagate_condition=False, + ) return dag def _check_not_in_basis(self, dag, gate_name, qargs): diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index e7c502c9ef9f..181f02e312b3 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -242,10 +242,8 @@ def run(self, dag): qubit = run[0].qargs for gate, angles in best_circuit_sequence: op = CircuitInstruction.from_standard(gate, qubit, angles) - node = DAGOpNode.from_instruction(op, dag=dag) - node._node_id = dag._multi_graph.add_node(node) - dag._increment_op(gate.name) - dag._multi_graph.insert_node_on_in_edges(node._node_id, first_node_id) + node = DAGOpNode.from_instruction(op) + dag._insert_1q_on_incoming_qubit(node, first_node_id) dag.global_phase += best_circuit_sequence.global_phase # Delete the other nodes in the run for current_node in run: diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py index fe6fe7f49e78..0a583259c800 100644 --- a/qiskit/transpiler/passes/optimization/optimize_annotated.py +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -125,7 +125,9 @@ def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]: # Handle control-flow for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) # First, optimize every node in the DAG. dag, opt1 = self._canonicalize(dag) @@ -163,7 +165,7 @@ def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: node.op.modifiers = canonical_modifiers else: # no need for annotated operations - node.op = cur + dag.substitute_node(node, cur, propagate_condition=False) did_something = True return dag, did_something diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 7508c9440a6e..ac04043a27fa 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -14,7 +14,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuitdata import CircuitInstruction -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +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 @@ -60,12 +61,12 @@ def run(self, dag: DAGCircuit): ur = decomp.K1r ur_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=new_dag + CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)) ) ul = decomp.K1l ul_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=new_dag + CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)) ) new_dag._apply_op_node_back(ur_node) new_dag._apply_op_node_back(ul_node) diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 788f0d995754..9edd1ceee445 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -372,7 +372,7 @@ def empty_dag(block): empty.add_clbits(block.clbits) for creg in block.cregs: empty.add_creg(creg) - empty._global_phase = block.global_phase + empty.global_phase = block.global_phase return empty def apply_swaps(dest_dag, swaps, layout): @@ -388,7 +388,7 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): the virtual qubit in the root source DAG that it is bound to.""" swap_map, node_order, node_block_results = result for node_id in node_order: - node = source_dag._multi_graph[node_id] + node = source_dag.node(node_id) if node_id in swap_map: apply_swaps(dest_dag, swap_map[node_id], layout) if not node.is_control_flow(): diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index a3ebbd6cbdde..efbd7e37f626 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -223,7 +223,7 @@ def _layer_permutation(self, dag, layer_partition, layout, qubit_subset, couplin int_layout = nlayout.NLayout(layout_mapping, num_qubits, coupling.size()) trial_circuit = DAGCircuit() # SWAP circuit for slice of swaps in this trial - trial_circuit.add_qubits(layout.get_virtual_bits()) + trial_circuit.add_qubits(list(layout.get_virtual_bits())) edges = np.asarray(coupling.get_edges(), dtype=np.uint32).ravel() cdist = coupling._dist_matrix @@ -273,9 +273,7 @@ def _layer_update(self, dag, layer, best_layout, best_depth, best_circuit): # Output any swaps if best_depth > 0: logger.debug("layer_update: there are swaps in this layer, depth %d", best_depth) - dag.compose( - best_circuit, qubits={bit: bit for bit in best_circuit.qubits}, inline_captures=True - ) + dag.compose(best_circuit, qubits=list(best_circuit.qubits), inline_captures=True) else: logger.debug("layer_update: there are no swaps in this layer") # Output this layer diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 0e351161f7df..2217d32f847c 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -370,7 +370,10 @@ def _pad( op = prev_node.op theta_l, phi_l, lam_l = op.params op.params = Optimize1qGates.compose_u3(theta, phi, lam, theta_l, phi_l, lam_l) - prev_node.op = op + new_prev_node = dag.substitute_node(prev_node, op, propagate_condition=False) + start_time = self.property_set["node_start_time"].pop(prev_node) + if start_time is not None: + self.property_set["node_start_time"][new_prev_node] = start_time sequence_gphase += phase else: # Don't do anything if there's no single-qubit gate to absorb the inverse diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 2c7c97ec856a..d9f3d77f2915 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -72,9 +72,9 @@ def _get_node_duration( # Note that node duration is updated (but this is analysis pass) op = node.op.to_mutable() op.duration = duration - node.op = op + dag.substitute_node(node, op, propagate_condition=False) else: - duration = node.op.duration + duration = node.duration if isinstance(duration, ParameterExpression): raise TranspilerError( diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 08ac932d8aeb..f4f70210b785 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -108,7 +108,7 @@ def run(self, dag: DAGCircuit): op = node.op.to_mutable() op.duration = duration op.unit = time_unit - node.op = op + dag.substitute_node(node, op, propagate_condition=False) self.property_set["time_unit"] = time_unit return dag diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 38c46547b12d..73a402dc202f 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -303,8 +303,10 @@ def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: # next check control flow elif node.is_control_flow(): - node.op = control_flow.map_blocks( - partial(self._run, tracker=tracker.copy()), node.op + dag.substitute_node( + node, + control_flow.map_blocks(partial(self._run, tracker=tracker.copy()), node.op), + propagate_condition=False, ) # now we are free to synthesize diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 4054a158d12a..e31f6918f452 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -52,7 +52,8 @@ RGate, ) from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.exceptions import QiskitError from qiskit.providers.models.backendproperties import BackendProperties from qiskit.quantum_info import Operator @@ -511,7 +512,7 @@ def _run_main_loop( for node in dag.op_nodes(): if node.name not in CONTROL_FLOW_OP_NAMES: continue - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( [ dag_to_circuit( self._run_main_loop( @@ -530,6 +531,7 @@ def _run_main_loop( for block in node.op.blocks ] ) + dag.substitute_node(node, new_op, propagate_condition=False) out_dag = dag.copy_empty_like() for node in dag.topological_op_nodes(): @@ -572,15 +574,13 @@ def _run_main_loop( user_gate_node._to_circuit_instruction().replace( params=user_gate_node.params, qubits=tuple(qubits[x] for x in qargs), - ), - dag=out_dag, + ) ) else: node = DAGOpNode.from_instruction( CircuitInstruction.from_standard( gate, tuple(qubits[x] for x in qargs), params - ), - dag=out_dag, + ) ) out_dag._apply_op_node_back(node) out_dag.global_phase += global_phase diff --git a/qiskit/transpiler/passes/utils/control_flow.py b/qiskit/transpiler/passes/utils/control_flow.py index 3739852b4c7e..27c3c83d53c7 100644 --- a/qiskit/transpiler/passes/utils/control_flow.py +++ b/qiskit/transpiler/passes/utils/control_flow.py @@ -45,7 +45,7 @@ def trivial_recurse(method): use :func:`map_blocks` as:: if isinstance(node.op, ControlFlowOp): - node.op = map_blocks(self.run, node.op) + dag.substitute_node(node, map_blocks(self.run, node.op)) from with :meth:`.BasePass.run`.""" @@ -55,7 +55,9 @@ def bound_wrapped_method(dag): return out(self, dag) for node in dag.op_nodes(ControlFlowOp): - node.op = map_blocks(bound_wrapped_method, node.op) + dag.substitute_node( + node, map_blocks(bound_wrapped_method, node.op), propagate_condition=False + ) return method(self, dag) return out diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 79493ae8ad25..8ea77f7ccd46 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -175,7 +175,7 @@ def _run_coupling_map(self, dag, wire_map, edges=None): # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): if isinstance(node.op, ControlFlowOp): - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( dag_to_circuit( self._run_coupling_map( circuit_to_dag(block), @@ -188,6 +188,7 @@ def _run_coupling_map(self, dag, wire_map, edges=None): ) for block in node.op.blocks ) + dag.substitute_node(node, new_op, propagate_condition=False) continue if len(node.qargs) != 2: continue @@ -222,7 +223,7 @@ def _run_target(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 isinstance(node.op, ControlFlowOp): - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( dag_to_circuit( self._run_target( circuit_to_dag(block), @@ -234,6 +235,7 @@ def _run_target(self, dag, wire_map): ) for block in node.op.blocks ) + dag.substitute_node(node, new_op, propagate_condition=False) continue if len(node.qargs) != 2: continue diff --git a/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py b/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py index 40390a02cade..acb748bb6f95 100644 --- a/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py +++ b/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py @@ -108,7 +108,7 @@ def _collect_potential_merges(dag, barriers): for next_barrier in barriers[1:]: # Ensure barriers are adjacent before checking if they are mergeable. - if dag._multi_graph.has_edge(end_of_barrier._node_id, next_barrier._node_id): + if dag._has_edge(end_of_barrier._node_id, next_barrier._node_id): # Remove all barriers that have already been included in this new barrier from the # set of ancestors/descendants as they will be removed from the new DAG when it is diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index e6ee03905d27..d933d38b5c4a 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -454,14 +454,12 @@ def _get_layered_instructions( clbits = new_clbits dag = circuit_to_dag(circuit) - dag.qubits = qubits - dag.clbits = clbits if justify == "none": for node in dag.topological_op_nodes(): nodes.append([node]) else: - nodes = _LayerSpooler(dag, justify, measure_map) + nodes = _LayerSpooler(dag, qubits, clbits, justify, measure_map) # Optionally remove all idle wires and instructions that are on them and # on them only. @@ -515,23 +513,25 @@ def _get_gate_span(qubits, node): def _any_crossover(qubits, node, nodes): """Return True .IFF. 'node' crosses over any 'nodes'.""" - gate_span = _get_gate_span(qubits, node) - all_indices = [] - for check_node in nodes: - if check_node != node: - all_indices += _get_gate_span(qubits, check_node) - return any(i in gate_span for i in all_indices) + return bool( + set(_get_gate_span(qubits, node)).intersection( + bit for check_node in nodes for bit in _get_gate_span(qubits, check_node) + ) + ) + + +_GLOBAL_NID = 0 class _LayerSpooler(list): """Manipulate list of layer dicts for _get_layered_instructions.""" - def __init__(self, dag, justification, measure_map): + def __init__(self, dag, qubits, clbits, justification, measure_map): """Create spool""" super().__init__() self.dag = dag - self.qubits = dag.qubits - self.clbits = dag.clbits + self.qubits = qubits + self.clbits = clbits self.justification = justification self.measure_map = measure_map self.cregs = [self.dag.cregs[reg] for reg in self.dag.cregs] @@ -660,6 +660,15 @@ def slide_from_right(self, node, index): def add(self, node, index): """Add 'node' where it belongs, starting the try at 'index'.""" + # Before we add the node, we set its node ID to be globally unique + # within this spooler. This is necessary because nodes may span + # layers (which are separate DAGs), and thus can falsely compare + # as equal if their contents and node IDs happen to be the same. + # This is particularly important for the matplotlib drawer, which + # keys several of its internal data structures with these nodes. + global _GLOBAL_NID # pylint: disable=global-statement + node._node_id = _GLOBAL_NID + _GLOBAL_NID += 1 if self.justification == "left": self.slide_from_left(node, index) else: diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index dae97b51b0a4..a6a111fe75e9 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -15,9 +15,14 @@ """ Visualization function for DAG circuit representation. """ + +import io +import subprocess + from rustworkx.visualization import graphviz_draw from qiskit.dagcircuit.dagnode import DAGOpNode, DAGInNode, DAGOutNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.circuit import Qubit, Clbit, ClassicalRegister from qiskit.circuit.classical import expr from qiskit.converters import dagdependency_to_circuit @@ -26,7 +31,48 @@ from .exceptions import VisualizationError +IMAGE_TYPES = { + "canon", + "cmap", + "cmapx", + "cmapx_np", + "dia", + "dot", + "fig", + "gd", + "gd2", + "gif", + "hpgl", + "imap", + "imap_np", + "ismap", + "jpe", + "jpeg", + "jpg", + "mif", + "mp", + "pcl", + "pdf", + "pic", + "plain", + "plain-ext", + "png", + "ps", + "ps2", + "svg", + "svgz", + "vml", + "vmlz", + "vrml", + "vtx", + "wbmp", + "xdor", + "xlib", +} + + @_optionals.HAS_GRAPHVIZ.require_in_call +@_optionals.HAS_PIL.require_in_call def dag_drawer(dag, scale=0.7, filename=None, style="color"): """Plot the directed acyclic graph (dag) to represent operation dependencies in a quantum circuit. @@ -48,6 +94,8 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): Raises: VisualizationError: when style is not recognized. InvalidFileError: when filename provided is not valid + ValueError: If the file extension for ``filename`` is not an image + type supported by Graphviz. Example: .. plot:: @@ -69,6 +117,7 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): dag = circuit_to_dag(circ) dag_drawer(dag) """ + from PIL import Image # NOTE: use type str checking to avoid potential cyclical import # the two tradeoffs ere that it will not handle subclasses and it is @@ -215,16 +264,53 @@ def edge_attr_func(edge): e["label"] = label return e - image_type = None + image_type = "png" if filename: if "." not in filename: raise InvalidFileError("Parameter 'filename' must be in format 'name.extension'") image_type = filename.split(".")[-1] - return graphviz_draw( - dag._multi_graph, - node_attr_func, - edge_attr_func, - graph_attrs, - filename, - image_type, - ) + if image_type not in IMAGE_TYPES: + raise ValueError( + "The specified value for the image_type argument, " + f"'{image_type}' is not a valid choice. It must be one of: " + f"{IMAGE_TYPES}" + ) + + if isinstance(dag, DAGCircuit): + dot_str = dag._to_dot( + graph_attrs, + node_attr_func, + edge_attr_func, + ) + + prog = "dot" + if not filename: + dot_result = subprocess.run( + [prog, "-T", image_type], + input=dot_str.encode("utf-8"), + capture_output=True, + encoding=None, + check=True, + text=False, + ) + dot_bytes_image = io.BytesIO(dot_result.stdout) + image = Image.open(dot_bytes_image) + return image + else: + subprocess.run( + [prog, "-T", image_type, "-o", filename], + input=dot_str, + check=True, + encoding="utf8", + text=True, + ) + return None + else: + return graphviz_draw( + dag._multi_graph, + node_attr_func, + edge_attr_func, + graph_attrs, + filename, + image_type, + ) diff --git a/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml b/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml new file mode 100644 index 000000000000..090a8226c111 --- /dev/null +++ b/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml @@ -0,0 +1,49 @@ +--- +features_transpiler: + - | + The implementation of the :class:`.DAGCircuit` has been rewritten in Rust. This rewrite of + the Python class should be fully API compatible with the previous Python implementation of + the class. While the class was previously implemented using + `rustworkx `__ and its underlying data graph structure existed + in Rust, the implementation of the class and all the data was stored in Python. This new + version of :class:`.DAGCircuit` stores a Rust native representation for all its data and + is fully implemented in Rust. This new implementation should be more efficient in memory + usage as it compresses the qubit and clbit representation for instructions at rest. + It also enables speed up for transpiler passes as they can fully manipulate a + :class:`.DAGCircuit` from Rust. +upgrade_transpiler: + - | + :class:`.DAGNode` objects (and its subclasses :class:`.DAGInNode`, :class:`.DAGOutNode`, and + :class:`.DAGOpNode`) no longer return references to the same underlying object from + :class:`.DAGCircuit` methods. This was never a guarantee before that all returned nodes would + be shared reference to the same object, but with the migration of the :class:`.DAGCircuit` to + Rust when a :class:`.DAGNode` a new :class:`.DAGNode` instance is generated on the fly when + a node is returned to Python. These objects will evaluate as equal using ``==`` or similar + checks that rely on ``__eq__`` but will no longer identify as the same object. + - | + The :class:`.DAGOpNode` instances returned from the :class:`.DAGCircuit` are no longer shared + references to the underlying data stored on the DAG. In previous release it was possible to + do something like:: + + for node in dag.op_nodes(): + node.op = new_op + + however this type of mutation was always unsound as it could break the DAG's internal caching + and cause corruption of the data structure. Instead you should use the API provided by + :class:`.DAGCircuit` for mutation such as :meth:`.DAGCircuit.substitute_node`, + :meth:`.DAGCircuit.substitute_node_with_dag`, or :meth:`.DAGCircuit.contract_node`. For example + the above code block would become:: + + for node in dag.op_nodes(): + dag.substitute_node(node, new_op) + + This is similar to an upgrade note from 1.2.0 where this was noted on for mutation of the + :attr:`.DAGOpNode.op` attribute, not the :class:`.DAGOpNode` itself. However in 1.3 this extends + to the entire object, not just it's inner ``op`` attribute. In general this type of mutation was + always unsound and not supported, but could previously have potentially worked in some cases. +fixes: + - | + Fixed an issue with :meth:`.DAGCircuit.apply_operation_back` and + :meth:`.DAGCircuit.apply_operation_front` where previously if you set a + :class:`.Clbit` object to the input for the ``qargs`` argument it would silently be accepted. + This has been fixed so the type mismatch is correctly identified and an exception is raised. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index c3a930aed1f2..edb3df63e46d 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -3320,19 +3320,19 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) # This should be in the first component self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] @@ -3394,19 +3394,19 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) # This should be in the first component self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] @@ -3477,26 +3477,26 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIsInstance(first_meas_node.op, Measure) self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): self.assertIn(qubit_map[op_node.qargs[0]], components[2]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIn(qubit_map[op_node.qargs[0]], components[2]) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 26f7e4788083..ef8050961066 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -18,7 +18,6 @@ import unittest from ddt import ddt, data -import rustworkx as rx from numpy import pi from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGInNode, DAGOutNode, DAGCircuitError @@ -55,18 +54,16 @@ def raise_if_dagcircuit_invalid(dag): DAGCircuitError: if DAGCircuit._multi_graph is inconsistent. """ - multi_graph = dag._multi_graph - - if not rx.is_directed_acyclic_graph(multi_graph): + if not dag._is_dag(): raise DAGCircuitError("multi_graph is not a DAG.") # Every node should be of type in, out, or op. # All input/output nodes should be present in input_map/output_map. - for node in dag._multi_graph.nodes(): + for node in dag.nodes(): if isinstance(node, DAGInNode): - assert node is dag.input_map[node.wire] + assert node == dag.input_map[node.wire] elif isinstance(node, DAGOutNode): - assert node is dag.output_map[node.wire] + assert node == dag.output_map[node.wire] elif isinstance(node, DAGOpNode): continue else: @@ -78,9 +75,7 @@ def raise_if_dagcircuit_invalid(dag): assert len(node.cargs) == node.op.num_clbits # Every edge should be labled with a known wire. - edges_outside_wires = [ - edge_data for edge_data in dag._multi_graph.edges() if edge_data not in dag.wires - ] + edges_outside_wires = [edge_data for edge_data in dag._edges() if edge_data not in dag.wires] if edges_outside_wires: raise DAGCircuitError( f"multi_graph contains one or more edges ({edges_outside_wires}) " @@ -103,7 +98,7 @@ def raise_if_dagcircuit_invalid(dag): out_node_id = dag.output_map[wire]._node_id while cur_node_id != out_node_id: - out_edges = dag._multi_graph.out_edges(cur_node_id) + out_edges = dag._out_edges(cur_node_id) edges_to_follow = [(src, dest, data) for (src, dest, data) in out_edges if data == wire] assert len(edges_to_follow) == 1 @@ -112,18 +107,15 @@ def raise_if_dagcircuit_invalid(dag): # Wires can only terminate at input/output nodes. op_counts = Counter() for op_node in dag.op_nodes(): - assert multi_graph.in_degree(op_node._node_id) == multi_graph.out_degree(op_node._node_id) + assert sum(1 for _ in dag.predecessors(op_node)) == sum(1 for _ in dag.successors(op_node)) op_counts[op_node.name] += 1 # The _op_names attribute should match the counted op names - assert op_counts == dag._op_names + assert op_counts == dag.count_ops() # Node input/output edges should match node qarg/carg/condition. for node in dag.op_nodes(): - in_edges = dag._multi_graph.in_edges(node._node_id) - out_edges = dag._multi_graph.out_edges(node._node_id) - - in_wires = {data for src, dest, data in in_edges} - out_wires = {data for src, dest, data in out_edges} + in_wires = set(dag._in_wires(node._node_id)) + out_wires = set(dag._out_wires(node._node_id)) node_cond_bits = set( node.op.condition[0][:] if getattr(node.op, "condition", None) is not None else [] @@ -516,6 +508,38 @@ def test_remove_unknown_clbit(self): self.assert_cregs_equal(self.original_cregs) self.assert_clbits_equal(self.original_clbits) + def test_remove_clbit_with_control_flow(self): + """Test clbit removal in the middle of clbits with control flow.""" + qr = QuantumRegister(1) + cr1 = ClassicalRegister(2, "a") + cr2 = ClassicalRegister(2, "b") + clbit = Clbit() + dag = DAGCircuit() + dag.add_qreg(qr) + dag.add_creg(cr1) + dag.add_creg(cr2) + dag.add_clbits([clbit]) + + inner = QuantumCircuit(1) + inner.h(0) + inner.z(0) + + op = IfElseOp(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, None) + dag.apply_operation_back(op, qr, ()) + dag.remove_clbits(*cr2) + self.assertEqual(dag.clbits, list(cr1) + [clbit]) + self.assertEqual(dag.cregs, {"a": cr1}) + + expected = DAGCircuit() + expected.add_qreg(qr) + expected.add_creg(cr1) + expected.add_clbits([clbit]) + + op = IfElseOp(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, None) + expected.apply_operation_back(op, qr, ()) + + self.assertEqual(dag, expected) + class TestDagApplyOperation(QiskitTestCase): """Test adding an op node to a dag.""" @@ -575,7 +599,7 @@ def test_apply_operation_back_conditional(self): self.assertEqual(h_node.op.condition, h_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(h_node._node_id)), + sorted(self.dag._in_edges(h_node._node_id)), sorted( [ (self.dag.input_map[self.qubit2]._node_id, h_node._node_id, self.qubit2), @@ -586,7 +610,7 @@ def test_apply_operation_back_conditional(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(h_node._node_id)), + sorted(self.dag._out_edges(h_node._node_id)), sorted( [ (h_node._node_id, self.dag.output_map[self.qubit2]._node_id, self.qubit2), @@ -596,7 +620,7 @@ def test_apply_operation_back_conditional(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_back_conditional_measure(self): """Test consistency of apply_operation_back for conditional measure.""" @@ -615,7 +639,7 @@ def test_apply_operation_back_conditional_measure(self): self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(meas_node._node_id)), + sorted(self.dag._in_edges(meas_node._node_id)), sorted( [ (self.dag.input_map[self.qubit0]._node_id, meas_node._node_id, self.qubit0), @@ -630,7 +654,7 @@ def test_apply_operation_back_conditional_measure(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(meas_node._node_id)), + sorted(self.dag._out_edges(meas_node._node_id)), sorted( [ (meas_node._node_id, self.dag.output_map[self.qubit0]._node_id, self.qubit0), @@ -644,7 +668,7 @@ def test_apply_operation_back_conditional_measure(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_back_conditional_measure_to_self(self): """Test consistency of apply_operation_back for measure onto conditioning bit.""" @@ -660,7 +684,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(meas_node._node_id)), + sorted(self.dag._in_edges(meas_node._node_id)), sorted( [ (self.dag.input_map[self.qubit1]._node_id, meas_node._node_id, self.qubit1), @@ -671,7 +695,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(meas_node._node_id)), + sorted(self.dag._out_edges(meas_node._node_id)), sorted( [ (meas_node._node_id, self.dag.output_map[self.qubit1]._node_id, self.qubit1), @@ -681,7 +705,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_front(self): """The apply_operation_front() method""" @@ -935,8 +959,8 @@ def test_classical_predecessors(self): self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(HGate(), [self.qubit1], []) - self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) - self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0], [self.clbit0]) + self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) predecessor_measure = self.dag.classical_predecessors(self.dag.named_nodes("measure").pop()) @@ -948,6 +972,13 @@ def test_classical_predecessors(self): self.assertIsInstance(predecessor1, DAGInNode) self.assertIsInstance(predecessor1.wire, Clbit) + def test_apply_operation_reject_invalid_qarg_carg(self): + """Test that we can't add a carg to qargs and vice versa on apply methods""" + with self.assertRaises(KeyError): + self.dag.apply_operation_back(Measure(), [self.clbit1], [self.qubit1]) + with self.assertRaises(KeyError): + self.dag.apply_operation_front(Measure(), [self.clbit1], [self.qubit1]) + def test_classical_successors(self): """The method dag.classical_successors() returns successors connected by classical edges""" @@ -969,8 +1000,8 @@ def test_classical_successors(self): self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(HGate(), [self.qubit1], []) - self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) - self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0], [self.clbit0]) + self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) successors_measure = self.dag.classical_successors(self.dag.named_nodes("measure").pop()) @@ -1067,12 +1098,12 @@ def test_topological_nodes(self): ("cx", (self.qubit2, self.qubit1)), ("cx", (self.qubit0, self.qubit2)), ("h", (self.qubit2,)), + cr[0], + cr[1], qr[0], qr[1], qr[2], cr[0], - cr[0], - cr[1], cr[1], ] self.assertEqual( @@ -2397,7 +2428,6 @@ def test_contract_var_use_to_nothing(self): expected = DAGCircuit() expected.add_input_var(a) - self.assertEqual(src, expected) def test_raise_if_var_mismatch(self): @@ -2665,7 +2695,6 @@ def test_substituting_node_preserves_args_condition(self, inplace): self.assertEqual(replacement_node.qargs, (qr[1], qr[0])) self.assertEqual(replacement_node.cargs, ()) self.assertEqual(replacement_node.op.condition, (cr, 1)) - self.assertEqual(replacement_node is node_to_be_replaced, inplace) @data(True, False) @@ -3172,24 +3201,34 @@ def test_creg_conditional(self): self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) self.assertEqual(gate_node.op.condition, (self.creg, 1)) + + gate_node_preds = list(self.dag.predecessors(gate_node)) + gate_node_in_edges = [ + (src._node_id, wire) + for (src, tgt, wire) in self.dag.edges(gate_node_preds) + if tgt == gate_node + ] + self.assertEqual( - sorted(self.dag._multi_graph.in_edges(gate_node._node_id)), + sorted(gate_node_in_edges), sorted( [ - (self.dag.input_map[self.qreg[0]]._node_id, gate_node._node_id, self.qreg[0]), - (self.dag.input_map[self.creg[0]]._node_id, gate_node._node_id, self.creg[0]), - (self.dag.input_map[self.creg[1]]._node_id, gate_node._node_id, self.creg[1]), + (self.dag.input_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.input_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.input_map[self.creg[1]]._node_id, self.creg[1]), ] ), ) + gate_node_out_edges = [(tgt._node_id, wire) for (_, tgt, wire) in self.dag.edges(gate_node)] + self.assertEqual( - sorted(self.dag._multi_graph.out_edges(gate_node._node_id)), + sorted(gate_node_out_edges), sorted( [ - (gate_node._node_id, self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[1]]._node_id, self.creg[1]), + (self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.output_map[self.creg[1]]._node_id, self.creg[1]), ] ), ) @@ -3204,22 +3243,31 @@ def test_clbit_conditional(self): self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) self.assertEqual(gate_node.op.condition, (self.creg[0], 1)) + + gate_node_preds = list(self.dag.predecessors(gate_node)) + gate_node_in_edges = [ + (src._node_id, wire) + for (src, tgt, wire) in self.dag.edges(gate_node_preds) + if tgt == gate_node + ] + self.assertEqual( - sorted(self.dag._multi_graph.in_edges(gate_node._node_id)), + sorted(gate_node_in_edges), sorted( [ - (self.dag.input_map[self.qreg[0]]._node_id, gate_node._node_id, self.qreg[0]), - (self.dag.input_map[self.creg[0]]._node_id, gate_node._node_id, self.creg[0]), + (self.dag.input_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.input_map[self.creg[0]]._node_id, self.creg[0]), ] ), ) + gate_node_out_edges = [(tgt._node_id, wire) for (_, tgt, wire) in self.dag.edges(gate_node)] self.assertEqual( - sorted(self.dag._multi_graph.out_edges(gate_node._node_id)), + sorted(gate_node_out_edges), sorted( [ - (gate_node._node_id, self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), ] ), ) diff --git a/test/python/transpiler/_dummy_passes.py b/test/python/transpiler/_dummy_passes.py index 64956e541024..eaddfe6d1ddd 100644 --- a/test/python/transpiler/_dummy_passes.py +++ b/test/python/transpiler/_dummy_passes.py @@ -122,10 +122,10 @@ class PassF_reduce_dag_property(DummyTP): def run(self, dag): super().run(dag) - if not hasattr(dag, "property"): - dag.property = 8 - dag.property = round(dag.property * 0.8) - logging.getLogger(logger).info("dag property = %i", dag.property) + if dag.duration is None: + dag.duration = 8 + dag.duration = round(dag.duration * 0.8) + logging.getLogger(logger).info("dag property = %i", dag.duration) return dag @@ -138,8 +138,8 @@ class PassG_calculates_dag_property(DummyAP): def run(self, dag): super().run(dag) - if hasattr(dag, "property"): - self.property_set["property"] = dag.property + if dag.duration is not None: + self.property_set["property"] = dag.duration else: self.property_set["property"] = 8 logging.getLogger(logger).info( diff --git a/test/python/transpiler/test_collect_multiq_blocks.py b/test/python/transpiler/test_collect_multiq_blocks.py index 58589d838e33..e2446d03da2a 100644 --- a/test/python/transpiler/test_collect_multiq_blocks.py +++ b/test/python/transpiler/test_collect_multiq_blocks.py @@ -93,6 +93,7 @@ def test_block_interrupted_by_gate(self): # but equivalent between python 3.5 and 3.7 # there is no implied topology in a block, so this isn't an issue dag_nodes = [set(dag_nodes[:4]), set(dag_nodes[4:])] + pass_nodes = [set(bl) for bl in pass_.property_set["block_list"]] self.assertEqual(dag_nodes, pass_nodes) diff --git a/test/python/visualization/test_utils.py b/test/python/visualization/test_utils.py index 1dca43b6beb2..1ef87994d001 100644 --- a/test/python/visualization/test_utils.py +++ b/test/python/visualization/test_utils.py @@ -48,18 +48,18 @@ def test_get_layered_instructions(self): (qregs, cregs, layered_ops) = _utils._get_layered_instructions(self.circuit) exp = [ - [("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())], - [("measure", (self.qr2[0],), (self.cr2[0],))], - [("measure", (self.qr1[0],), (self.cr1[0],))], - [("cx", (self.qr2[1], self.qr2[0]), ()), ("cx", (self.qr1[1], self.qr1[0]), ())], - [("measure", (self.qr2[1],), (self.cr2[1],))], - [("measure", (self.qr1[1],), (self.cr1[1],))], + {("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())}, + {("measure", (self.qr2[0],), (self.cr2[0],))}, + {("measure", (self.qr1[0],), (self.cr1[0],))}, + {("cx", (self.qr2[1], self.qr2[0]), ()), ("cx", (self.qr1[1], self.qr1[0]), ())}, + {("measure", (self.qr2[1],), (self.cr2[1],))}, + {("measure", (self.qr1[1],), (self.cr1[1],))}, ] self.assertEqual([self.qr1[0], self.qr1[1], self.qr2[0], self.qr2[1]], qregs) self.assertEqual([self.cr1[0], self.cr1[1], self.cr2[0], self.cr2[1]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_reverse_bits(self): @@ -69,18 +69,18 @@ def test_get_layered_instructions_reverse_bits(self): ) exp = [ - [("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())], - [("measure", (self.qr2[0],), (self.cr2[0],))], - [("measure", (self.qr1[0],), (self.cr1[0],)), ("cx", (self.qr2[1], self.qr2[0]), ())], - [("cx", (self.qr1[1], self.qr1[0]), ())], - [("measure", (self.qr2[1],), (self.cr2[1],))], - [("measure", (self.qr1[1],), (self.cr1[1],))], + {("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())}, + {("measure", (self.qr2[0],), (self.cr2[0],))}, + {("measure", (self.qr1[0],), (self.cr1[0],)), ("cx", (self.qr2[1], self.qr2[0]), ())}, + {("cx", (self.qr1[1], self.qr1[0]), ())}, + {("measure", (self.qr2[1],), (self.cr2[1],))}, + {("measure", (self.qr1[1],), (self.cr1[1],))}, ] self.assertEqual([self.qr2[1], self.qr2[0], self.qr1[1], self.qr1[0]], qregs) self.assertEqual([self.cr2[1], self.cr2[0], self.cr1[1], self.cr1[0]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_remove_idle_wires(self): @@ -103,18 +103,18 @@ def test_get_layered_instructions_remove_idle_wires(self): (qregs, cregs, layered_ops) = _utils._get_layered_instructions(circuit, idle_wires=False) exp = [ - [("cx", (qr2[0], qr2[1]), ()), ("cx", (qr1[0], qr1[1]), ())], - [("measure", (qr2[0],), (cr2[0],))], - [("measure", (qr1[0],), (cr1[0],))], - [("cx", (qr2[1], qr2[0]), ()), ("cx", (qr1[1], qr1[0]), ())], - [("measure", (qr2[1],), (cr2[1],))], - [("measure", (qr1[1],), (cr1[1],))], + {("cx", (qr2[0], qr2[1]), ()), ("cx", (qr1[0], qr1[1]), ())}, + {("measure", (qr2[0],), (cr2[0],))}, + {("measure", (qr1[0],), (cr1[0],))}, + {("cx", (qr2[1], qr2[0]), ()), ("cx", (qr1[1], qr1[0]), ())}, + {("measure", (qr2[1],), (cr2[1],))}, + {("measure", (qr1[1],), (cr1[1],))}, ] self.assertEqual([qr1[0], qr1[1], qr2[0], qr2[1]], qregs) self.assertEqual([cr1[0], cr1[1], cr2[0], cr2[1]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_left_justification_simple(self): @@ -136,15 +136,15 @@ def test_get_layered_instructions_left_justification_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="left") l_exp = [ - [ + { ("h", (Qubit(QuantumRegister(4, "q"), 1),), ()), ("h", (Qubit(QuantumRegister(4, "q"), 2),), ()), - ], - [("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())], + }, + {("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())}, ] self.assertEqual( - l_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + l_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_right_justification_simple(self): @@ -166,15 +166,15 @@ def test_get_layered_instructions_right_justification_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="right") r_exp = [ - [("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())], - [ + {("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())}, + { ("h", (Qubit(QuantumRegister(4, "q"), 1),), ()), ("h", (Qubit(QuantumRegister(4, "q"), 2),), ()), - ], + }, ] self.assertEqual( - r_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + r_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_left_justification_less_simple(self): @@ -215,33 +215,33 @@ def test_get_layered_instructions_left_justification_less_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="left") l_exp = [ - [ + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("u2", (Qubit(QuantumRegister(5, "q"), 1),), ())], - [ + }, + {("u2", (Qubit(QuantumRegister(5, "q"), 1),), ())}, + { ( "measure", (Qubit(QuantumRegister(5, "q"), 0),), (Clbit(ClassicalRegister(1, "c1"), 0),), ) - ], - [("u2", (Qubit(QuantumRegister(5, "q"), 0),), ())], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("u2", (Qubit(QuantumRegister(5, "q"), 0),), ())}, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], + }, ] self.assertEqual( - l_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + l_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_right_justification_less_simple(self): @@ -282,35 +282,35 @@ def test_get_layered_instructions_right_justification_less_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="right") r_exp = [ - [ + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [ + }, + { ( "measure", (Qubit(QuantumRegister(5, "q"), 0),), (Clbit(ClassicalRegister(1, "c1"), 0),), ) - ], - [ + }, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], + }, ] self.assertEqual( - r_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + r_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_op_with_cargs(self): @@ -335,25 +335,25 @@ def test_get_layered_instructions_op_with_cargs(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc) expected = [ - [("h", (Qubit(QuantumRegister(2, "q"), 0),), ())], - [ + {("h", (Qubit(QuantumRegister(2, "q"), 0),), ())}, + { ( "measure", (Qubit(QuantumRegister(2, "q"), 0),), (Clbit(ClassicalRegister(2, "c"), 0),), ) - ], - [ + }, + { ( "add_circ", (Qubit(QuantumRegister(2, "q"), 1),), (Clbit(ClassicalRegister(2, "c"), 0),), ) - ], + }, ] self.assertEqual( - expected, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + expected, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc")