Skip to content

Commit

Permalink
Add Rust representation of EquivalenceLibrary (#12585)
Browse files Browse the repository at this point in the history
* Initial: Add equivalence to `qiskit._accelerate.circuit`

* Add: `build_basis_graph` method

* Add: `EquivalencyLibrary` to `qiskit._accelerate.circuit`
- Add `get_entry` method to obtain an entry from binding to a `QuantumCircuit`.
- Add `rebind_equiv` to bind parameters to `QuantumCircuit`

* Add: PyDiGraph converter for `equivalence.rs`

* Add: Extend original equivalence with rust representation

* Fix: Correct circuit parameter extraction

* Add: Stable infrastructure for EquivalenceLibrary
- TODO: Make elements pickleable.

* Add: Default methods to equivalence data structures.

* Fix: Adapt to new Gate Structure

* Fix: Erroneous display of `Parameters`

* Format: Fix lint test

* Fix: Use EdgeReferences instead of edge_indices.
- Remove stray comment.
- Use match instead of matches!.

* Fix: Use StableDiGraph for more stable indexing.
- Remove required py argument for get_entry.
- Reformat `to_pygraph` to use `add_nodes_from` and `add_edges_from`.
- Other small fixes.

* Fix: Use `clone` instead of `to_owned`
- Use `clone_ref` for the PyObject Graph instance.

* Fix: Use `OperationTypeConstruct` instead of `CircuitInstruction`
- Use `convert_py_to_operation_type` to correctly extract Gate instances into rust operational datatypes.
- Add function `get_sources_from_circuit_rep` to not extract circuit data directly but only the necessary data.
- Modify failing test due to different mapping. (!!)
- Other tweaks and fixes.

* Fix: Elide implicit lifetime of PyRef

* Fix: Make `CircuitRep` attributes OneCell-like.
- Attributes from CircuitRep are only written once, reducing the overhead.
- Modify `__setstate__` to avoid extra conversion.
- Remove `get_sources_from_circuit_rep`.

* Fix: Incorrect pickle attribute extraction

* Remove: Default initialization methods from custom datatypes.
- Use `__getnewargs__ instead.

* Remove: `__getstate__`, `__setstate__`, use `__getnewargs__` instead.

* Fix: Further improvements to pickling
- Use python structures to avoid extra conversions.
- Add rust native `EquivalenceLibrary.keys()` and have the python method use it.

* Fix: Use `PyList` and iterators when possible to skip extra conversion.
- Use a `py` token instead of `Python::with_gil()` for `rebind_params`.
- Other tweaks and fixes.

* Fix: incorrect list operation in `__getstate__`

* Fix: improvements on rust native methods
- Accept `Operations` and `[Param]` instead of the custom `GateOper` when calling from rust.
- Build custom `GateOper` inside of class.

* Remove: `add_equiv`, `set_entry` from rust-native methods.
- Add `node_index` Rust native method.
- Use python set comparison for `Param` check.

* Remove: Undo changes to Param
- Fix comparison methods for `Key`, `Equivalence`, `EdgeData` and `NodeData` to account for the removal of `PartialEq` for `Param`.

* Fix: Leverage usage of `CircuitData` for accessing the `QuantumCircuit` intructions in rust.
- Change implementation of `CircuitRef, to leverage the use of `CircuitData`.

* Add: `data()` method to avoid extracting `CircuitData`
- Add `py_clone` to perform shallow clones of a `CircuitRef` object by cloning the references to the `QuantumCircuit` object.
- Extract `num_qubits` and `num_clbits` for CircuitRep.
- Add wrapper over `add_equivalence` to be able to accept references and avoid unnecessary cloning of `GateRep` objects in `set_entry`.
- Remove stray mutability of `entry` in `set_entry`.

* Fix: Make `graph` attribute public.

* Fix: Make `NoteData` attributes public.

* Fix: Revert reference to `CircuitData`, extract instead.

* Add: Make `EquivalenceLibrary` graph weights optional.

* Fix: Adapt to #12730

* Fix: Use `IndexSet` and `IndexMap`

* Fix: Revert changes from previously failing test

* Fix: Adapt to #12974

* Fix: Use `EquivalenceLibrary.keys()` instead of `._key_to_node_index`

* Chore: update dependencies

* Refactor: Move `EquivalenceLibrary` to `_accelerate`.

* Fix: Erroneous `pymodule` function for `equivalence`.

* Fix: Update `EquivalenceLibrary` to store `CircuitData`.
- The equivalence library will now only store `CircuitData` instances as it does not need to call python directly to re-assign parameters.
- An `IntoPy<PyObject>` trait was adapted so that it can be automatically converted to a `QuantumCircuit` instance using `_from_circuit_data`.
- Updated all tests to use register-less qubits in circuit comparison.
- Remove `num_qubits` and `num_clbits` from `CircuitRep`.

* Fix: Make inner `CircuitData` instance public.

* Fix: Review comments and ownership issues.
- Add `from_operation` constructor for `Key`.
- Made `py_has_entry()` private, but kept its main method public.
- Made `set_entry` more rust friendly.
- Modify `add_equivalence` to accept a slice of `Param` and use `Into` to convert it into a `SmallVec` instance.

* Fix: Use maximum possible integer value for Key in basis_translator.
- Add method to immutably borrow the `EquivalenceLibrary`'s graph.

* Fix: Use generated string, instead of large int
- Using large int as the key's number of qubits breaks compatibility with qpy, use a random string instead.

---------

Co-authored-by: John Lapeyre <[email protected]>
  • Loading branch information
raynelfss and jlapeyre authored Sep 25, 2024
1 parent dcd41e9 commit 6a041ee
Show file tree
Hide file tree
Showing 10 changed files with 877 additions and 263 deletions.
805 changes: 805 additions & 0 deletions crates/accelerate/src/equivalence.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/accelerate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod commutation_checker;
pub mod convert_2q_block_matrix;
pub mod dense_layout;
pub mod edge_collections;
pub mod equivalence;
pub mod error_map;
pub mod euler_one_qubit_decomposer;
pub mod filter_op_nodes;
Expand Down
2 changes: 1 addition & 1 deletion crates/circuit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ workspace = true
features = ["union"]

[features]
cache_pygates = []
cache_pygates = []
1 change: 1 addition & 0 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
add_submodule(m, ::qiskit_accelerate::commutation_checker::commutation_checker, "commutation_checker")?;
add_submodule(m, ::qiskit_accelerate::convert_2q_block_matrix::convert_2q_block_matrix, "convert_2q_block_matrix")?;
add_submodule(m, ::qiskit_accelerate::dense_layout::dense_layout, "dense_layout")?;
add_submodule(m, ::qiskit_accelerate::equivalence::equivalence, "equivalence")?;
add_submodule(m, ::qiskit_accelerate::error_map::error_map, "error_map")?;
add_submodule(m, ::qiskit_accelerate::euler_one_qubit_decomposer::euler_one_qubit_decomposer, "euler_one_qubit_decomposer")?;
add_submodule(m, ::qiskit_accelerate::filter_op_nodes::filter_op_nodes_mod, "filter_op_nodes")?;
Expand Down
1 change: 1 addition & 0 deletions qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
sys.modules["qiskit._accelerate.converters"] = _accelerate.converters
sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix
sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout
sys.modules["qiskit._accelerate.equivalence"] = _accelerate.equivalence
sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map
sys.modules["qiskit._accelerate.isometry"] = _accelerate.isometry
sys.modules["qiskit._accelerate.uc_gate"] = _accelerate.uc_gate
Expand Down
227 changes: 13 additions & 214 deletions qiskit/circuit/equivalence.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,190 +12,24 @@

"""Gate equivalence library."""

import copy
from collections import namedtuple

from rustworkx.visualization import graphviz_draw
import rustworkx as rx

from qiskit.exceptions import InvalidFileError
from .exceptions import CircuitError
from .parameter import Parameter
from .parameterexpression import ParameterExpression

Key = namedtuple("Key", ["name", "num_qubits"])
Equivalence = namedtuple("Equivalence", ["params", "circuit"]) # Ordered to match Gate.params
NodeData = namedtuple("NodeData", ["key", "equivs"])
EdgeData = namedtuple("EdgeData", ["index", "num_gates", "rule", "source"])
from qiskit.exceptions import InvalidFileError
from qiskit._accelerate.equivalence import ( # pylint: disable=unused-import
BaseEquivalenceLibrary,
Key,
Equivalence,
NodeData,
EdgeData,
)


class EquivalenceLibrary:
class EquivalenceLibrary(BaseEquivalenceLibrary):
"""A library providing a one-way mapping of Gates to their equivalent
implementations as QuantumCircuits."""

def __init__(self, *, base=None):
"""Create a new equivalence library.
Args:
base (Optional[EquivalenceLibrary]): Base equivalence library to
be referenced if an entry is not found in this library.
"""
self._base = base

if base is None:
self._graph = rx.PyDiGraph()
self._key_to_node_index = {}
# Some unique identifier for rules.
self._rule_id = 0
else:
self._graph = base._graph.copy()
self._key_to_node_index = copy.deepcopy(base._key_to_node_index)
self._rule_id = base._rule_id

@property
def graph(self) -> rx.PyDiGraph:
"""Return graph representing the equivalence library data.
This property should be treated as read-only as it provides
a reference to the internal state of the :class:`~.EquivalenceLibrary` object.
If the graph returned by this property is mutated it could corrupt the
the contents of the object. If you need to modify the output ``PyDiGraph``
be sure to make a copy prior to any modification.
Returns:
PyDiGraph: A graph object with equivalence data in each node.
"""
return self._graph

def _set_default_node(self, key):
"""Create a new node if key not found"""
if key not in self._key_to_node_index:
self._key_to_node_index[key] = self._graph.add_node(NodeData(key=key, equivs=[]))
return self._key_to_node_index[key]

def add_equivalence(self, gate, equivalent_circuit):
"""Add a new equivalence to the library. Future queries for the Gate
will include the given circuit, in addition to all existing equivalences
(including those from base).
Parameterized Gates (those including `qiskit.circuit.Parameters` in their
`Gate.params`) can be marked equivalent to parameterized circuits,
provided the parameters match.
Args:
gate (Gate): A Gate instance.
equivalent_circuit (QuantumCircuit): A circuit equivalently
implementing the given Gate.
"""

_raise_if_shape_mismatch(gate, equivalent_circuit)
_raise_if_param_mismatch(gate.params, equivalent_circuit.parameters)

key = Key(name=gate.name, num_qubits=gate.num_qubits)
equiv = Equivalence(params=gate.params.copy(), circuit=equivalent_circuit.copy())

target = self._set_default_node(key)
self._graph[target].equivs.append(equiv)

sources = {
Key(name=instruction.operation.name, num_qubits=len(instruction.qubits))
for instruction in equivalent_circuit
}
edges = [
(
self._set_default_node(source),
target,
EdgeData(index=self._rule_id, num_gates=len(sources), rule=equiv, source=source),
)
for source in sources
]
self._graph.add_edges_from(edges)
self._rule_id += 1

def has_entry(self, gate):
"""Check if a library contains any decompositions for gate.
Args:
gate (Gate): A Gate instance.
Returns:
Bool: True if gate has a known decomposition in the library.
False otherwise.
"""
key = Key(name=gate.name, num_qubits=gate.num_qubits)

return key in self._key_to_node_index

def set_entry(self, gate, entry):
"""Set the equivalence record for a Gate. Future queries for the Gate
will return only the circuits provided.
Parameterized Gates (those including `qiskit.circuit.Parameters` in their
`Gate.params`) can be marked equivalent to parameterized circuits,
provided the parameters match.
Args:
gate (Gate): A Gate instance.
entry (List['QuantumCircuit']) : A list of QuantumCircuits, each
equivalently implementing the given Gate.
"""
for equiv in entry:
_raise_if_shape_mismatch(gate, equiv)
_raise_if_param_mismatch(gate.params, equiv.parameters)

node_index = self._set_default_node(Key(name=gate.name, num_qubits=gate.num_qubits))
# Remove previous equivalences of this node, leaving in place any later equivalences that
# were added that use `gate`.
self._graph[node_index].equivs.clear()
for parent, child, _ in self._graph.in_edges(node_index):
# `child` should always be ourselves, but there might be parallel edges.
self._graph.remove_edge(parent, child)
for equivalence in entry:
self.add_equivalence(gate, equivalence)

def get_entry(self, gate):
"""Gets the set of QuantumCircuits circuits from the library which
equivalently implement the given Gate.
Parameterized circuits will have their parameters replaced with the
corresponding entries from Gate.params.
Args:
gate (Gate) - Gate: A Gate instance.
Returns:
List[QuantumCircuit]: A list of equivalent QuantumCircuits. If empty,
library contains no known decompositions of Gate.
Returned circuits will be ordered according to their insertion in
the library, from earliest to latest, from top to base. The
ordering of the StandardEquivalenceLibrary will not generally be
consistent across Qiskit versions.
"""
key = Key(name=gate.name, num_qubits=gate.num_qubits)
query_params = gate.params

return [_rebind_equiv(equiv, query_params) for equiv in self._get_equivalences(key)]

def keys(self):
"""Return list of keys to key to node index map.
Returns:
List: Keys to the key to node index map.
"""
return self._key_to_node_index.keys()

def node_index(self, key):
"""Return node index for a given key.
Args:
key (Key): Key to an equivalence.
Returns:
Int: Index to the node in the graph for the given key.
"""
return self._key_to_node_index[key]

def draw(self, filename=None):
"""Draws the equivalence relations available in the library.
Expand Down Expand Up @@ -227,12 +61,13 @@ def _build_basis_graph(self):
graph = rx.PyDiGraph()

node_map = {}
for key in self._key_to_node_index:
name, num_qubits = key
for key in super().keys():
name, num_qubits = key.name, key.num_qubits
equivalences = self._get_equivalences(key)

basis = frozenset([f"{name}/{num_qubits}"])
for params, decomp in equivalences:
for equivalence in equivalences:
params, decomp = equivalence.params, equivalence.circuit
decomp_basis = frozenset(
f"{name}/{num_qubits}"
for name, num_qubits in {
Expand All @@ -257,39 +92,3 @@ def _build_basis_graph(self):
)

return graph

def _get_equivalences(self, key):
"""Get all the equivalences for the given key"""
return (
self._graph[self._key_to_node_index[key]].equivs
if key in self._key_to_node_index
else []
)


def _raise_if_param_mismatch(gate_params, circuit_parameters):
gate_parameters = [p for p in gate_params if isinstance(p, ParameterExpression)]

if set(gate_parameters) != circuit_parameters:
raise CircuitError(
"Cannot add equivalence between circuit and gate "
f"of different parameters. Gate params: {gate_parameters}. "
f"Circuit params: {circuit_parameters}."
)


def _raise_if_shape_mismatch(gate, circuit):
if gate.num_qubits != circuit.num_qubits or gate.num_clbits != circuit.num_clbits:
raise CircuitError(
"Cannot add equivalence between circuit and gate "
f"of different shapes. Gate: {gate.num_qubits} qubits and {gate.num_clbits} clbits. "
f"Circuit: {circuit.num_qubits} qubits and {circuit.num_clbits} clbits."
)


def _rebind_equiv(equiv, query_params):
equiv_params, equiv_circuit = equiv
param_map = {x: y for x, y in zip(equiv_params, query_params) if isinstance(x, Parameter)}
equiv = equiv_circuit.assign_parameters(param_map, inplace=False, flat_input=True)

return equiv
12 changes: 9 additions & 3 deletions qiskit/transpiler/passes/basis/basis_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

"""Translates gates to a target basis using a given equivalence library."""

import random
import time
import logging

Expand All @@ -32,7 +33,7 @@
)
from qiskit.dagcircuit import DAGCircuit, DAGOpNode
from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit.circuit.equivalence import Key, NodeData
from qiskit.circuit.equivalence import Key, NodeData, Equivalence
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES
Expand Down Expand Up @@ -559,7 +560,7 @@ def _basis_search(equiv_lib, source_basis, target_basis):
logger.debug("Begining basis search from %s to %s.", source_basis, target_basis)

source_basis = {
(gate_name, gate_num_qubits)
Key(gate_name, gate_num_qubits)
for gate_name, gate_num_qubits in source_basis
if gate_name not in target_basis
}
Expand All @@ -577,7 +578,12 @@ def _basis_search(equiv_lib, source_basis, target_basis):

# we add a dummy node and connect it with gates in the target basis.
# we'll start the search from this dummy node.
dummy = graph.add_node(NodeData(key="key", equivs=[("dummy starting node", 0)]))
dummy = graph.add_node(
NodeData(
key=Key("".join(chr(random.randint(0, 26) + 97) for _ in range(10)), 0),
equivs=[Equivalence([], QuantumCircuit(0, name="dummy starting node"))],
)
)

try:
graph.add_edges_from_no_data(
Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/synthesis/high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ def _definitely_skip_node(
or (
self._equiv_lib is not None
and equivalence.Key(name=node.name, num_qubits=node.num_qubits)
in self._equiv_lib._key_to_node_index
in self._equiv_lib.keys()
)
)
)
Expand Down
Loading

0 comments on commit 6a041ee

Please sign in to comment.