Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
a8cddcd
Initial: Add equivalence to `qiskit._accelerate.circuit`
raynelfss Jun 5, 2024
f53b89c
Add: `build_basis_graph` method
raynelfss Jun 10, 2024
a215694
Add: `EquivalencyLibrary` to `qiskit._accelerate.circuit`
raynelfss Jun 11, 2024
7d0f4de
Add: PyDiGraph converter for `equivalence.rs`
raynelfss Jun 12, 2024
0f85dc0
Merge branch 'main' into move-equivalence
raynelfss Jun 12, 2024
f73581e
Add: Extend original equivalence with rust representation
raynelfss Jun 12, 2024
57e0ef8
Fix: Correct circuit parameter extraction
raynelfss Jun 13, 2024
9eddbbf
Merge branch 'Qiskit:main' into move-equivalence
raynelfss Jun 13, 2024
11611e2
Add: Stable infrastructure for EquivalenceLibrary
raynelfss Jun 13, 2024
a013d5b
Merge branch 'main' into move-equivalence
raynelfss Jun 14, 2024
1bf0316
Add: Default methods to equivalence data structures.
raynelfss Jun 14, 2024
ab57e3e
Fix: Adapt to new Gate Structure
raynelfss Jun 14, 2024
0e822b5
Fix: Erroneous display of `Parameters`
raynelfss Jun 15, 2024
6a033c9
Format: Fix lint test
raynelfss Jun 17, 2024
b04cae2
Fix: Use EdgeReferences instead of edge_indices.
raynelfss Jun 17, 2024
5a3bf2b
Merge branch 'main' into move-equivalence
raynelfss Jun 18, 2024
38436cd
Fix: Use StableDiGraph for more stable indexing.
raynelfss Jun 21, 2024
db6acb4
Merge branch 'main' into move-equivalence
raynelfss Jun 21, 2024
e16e57b
Fix: Use `clone` instead of `to_owned`
raynelfss Jun 21, 2024
b8bd422
Merge branch 'Qiskit:main' into move-equivalence
raynelfss Jun 24, 2024
fb1652c
Fix: Use `OperationTypeConstruct` instead of `CircuitInstruction`
raynelfss Jun 24, 2024
139af74
Merge branch 'main' into move-equivalence
raynelfss Jun 24, 2024
27b25e3
Merge branch 'main' into move-equivalence
raynelfss Jun 25, 2024
ab84d04
Merge branch 'Qiskit:main' into move-equivalence
raynelfss Jun 26, 2024
6dec921
Fix: Elide implicit lifetime of PyRef
raynelfss Jun 26, 2024
785564f
Merge branch 'main' into move-equivalence
raynelfss Jun 26, 2024
3b954e4
Fix: Make `CircuitRep` attributes OneCell-like.
raynelfss Jun 26, 2024
48bb8eb
Merge branch 'main' into move-equivalence
raynelfss Jun 26, 2024
ffa0a81
Merge branch 'main' into move-equivalence
raynelfss Jun 27, 2024
9a7d9a0
Fix: Incorrect pickle attribute extraction
raynelfss Jun 28, 2024
e2c8dcb
Merge branch 'main' into move-equivalence
raynelfss Jun 28, 2024
dc3041e
Remove: Default initialization methods from custom datatypes.
raynelfss Jun 28, 2024
a072635
Remove: `__getstate__`, `__setstate__`, use `__getnewargs__` instead.
raynelfss Jun 28, 2024
77dbec8
Fix: Further improvements to pickling
raynelfss Jul 1, 2024
cd0b36e
Merge branch 'main' into move-equivalence
raynelfss Jul 1, 2024
73e5b5b
Merge branch 'main' into move-equivalence
raynelfss Jul 1, 2024
72e1c33
Fix: Use `PyList` and iterators when possible to skip extra conversion.
raynelfss Jul 2, 2024
17eb9d2
Fix: incorrect list operation in `__getstate__`
raynelfss Jul 3, 2024
9d39cf6
Fix: improvements on rust native methods
raynelfss Jul 3, 2024
e9e3921
Remove: `add_equiv`, `set_entry` from rust-native methods.
raynelfss Jul 3, 2024
5519659
Merge branch 'main' into move-equivalence
raynelfss Jul 3, 2024
bf9aedd
Remove: Undo changes to Param
raynelfss Jul 5, 2024
82a456e
Merge branch 'main' into move-equivalence
raynelfss Jul 11, 2024
41764f2
Merge branch 'main' into move-equivalence
raynelfss Jul 12, 2024
08915c0
Fix: Leverage usage of `CircuitData` for accessing the `QuantumCircui…
raynelfss Jul 12, 2024
d24c134
Add: `data()` method to avoid extracting `CircuitData`
raynelfss Jul 15, 2024
bd582f0
Merge branch 'main' into move-equivalence
raynelfss Jul 15, 2024
f61288f
Merge branch 'main' into move-equivalence
raynelfss Jul 16, 2024
5a2c57b
Merge branch 'main' into move-equivalence
raynelfss Jul 18, 2024
6772305
Fix: Make `graph` attribute public.
raynelfss Jul 19, 2024
1e44817
Fix: Make `NoteData` attributes public.
raynelfss Jul 19, 2024
22210e3
Fix: Revert reference to `CircuitData`, extract instead.
raynelfss Jul 21, 2024
c99ca3f
Add: Make `EquivalenceLibrary` graph weights optional.
raynelfss Jul 21, 2024
561553a
Merge branch 'Qiskit:main' into move-equivalence
raynelfss Jul 23, 2024
d257db1
Merge branch 'main' into move-equivalence
raynelfss Jul 24, 2024
dd2773a
Fix: Adapt to #12730
raynelfss Jul 24, 2024
e7b3cb0
Fix: Use `IndexSet` and `IndexMap`
raynelfss Jul 24, 2024
aa626b9
Fix: Revert changes from previously failing test
raynelfss Jul 25, 2024
1aaf17a
Merge branch 'main' into move-equivalence
raynelfss Jul 25, 2024
ba90411
Merge branch 'main' into move-equivalence
raynelfss Jul 25, 2024
9f4bfb7
Merge branch 'main' into move-equivalence
raynelfss Jul 25, 2024
d59f0db
Merge branch 'main' into move-equivalence
raynelfss Aug 1, 2024
0d3a164
Fix: Adapt to #12974
raynelfss Aug 1, 2024
feff6eb
Fix: Use `EquivalenceLibrary.keys()` instead of `._key_to_node_index`
raynelfss Aug 2, 2024
6554b38
Merge branch 'main' into move-equivalence
raynelfss Aug 2, 2024
132b6be
Chore: update dependencies
raynelfss Aug 2, 2024
3dbcc41
Refactor: Move `EquivalenceLibrary` to `_accelerate`.
raynelfss Aug 6, 2024
c48f0da
Merge branch 'main' into move-equivalence
raynelfss Aug 6, 2024
d12dee6
Fix: Erroneous `pymodule` function for `equivalence`.
raynelfss Aug 6, 2024
bc9c83f
Merge branch 'main' into move-equivalence
raynelfss Aug 10, 2024
fda119d
Merge branch 'main' into move-equivalence
raynelfss Aug 29, 2024
f44eaba
Merge branch 'main' into move-equivalence
raynelfss Aug 30, 2024
3479254
Fix: Update `EquivalenceLibrary` to store `CircuitData`.
raynelfss Aug 30, 2024
c433c87
Fix: Make inner `CircuitData` instance public.
raynelfss Aug 30, 2024
c77af69
Merge branch 'main' into move-equivalence
raynelfss Sep 4, 2024
82a6dcf
Merge branch 'main' into move-equivalence
raynelfss Sep 5, 2024
1a21204
Merge branch 'main' into move-equivalence-gjl
jlapeyre Sep 18, 2024
b7c6316
Fix: Review comments and ownership issues.
raynelfss Sep 20, 2024
b8e7cff
Merge branch 'main' into move-equivalence
raynelfss Sep 20, 2024
e7d99bc
Fix: Use maximum possible integer value for Key in basis_translator.
raynelfss Sep 23, 2024
bf7486c
Fix: Use generated string, instead of large int
raynelfss Sep 24, 2024
56278bc
Merge branch 'main' into move-equivalence
raynelfss Sep 24, 2024
11f0470
Merge branch 'main' into move-equivalence
raynelfss Sep 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some things, eg. Key and NodeData, are not used apparently, and the lint for this is suppressed. Why is this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is to allow the user to still be able to import these things directly from qiskit.circuit.equivalence as they were able to before. If this is not needed anymore I can easily remove.

Copy link
Copy Markdown
Contributor

@jlapeyre jlapeyre Sep 19, 2024

Choose a reason for hiding this comment

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

Oh sorry, I did not check who calls these. Key and NodeData are called from Python in class BasisTranslator. Looks like they cannot be made private after all.

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
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