Skip to content

Commit

Permalink
Add support for semantic equality checking of Exprs in DAGCircuits
Browse files Browse the repository at this point in the history
This adds support to semantically check the variables of `Expr` nodes
that occur within `DAGCircuit` constructs, using similar logic to the
remapping done by the existing condition checks.  As a knock-on effect
of how the control-flow introspection was added to allow these checks,
the canonicalisation performed by
`qiskit.test._canonical.canonicalize_control_flow` is now inherent to
the `DAGCircuit` equality methods, largely removing the need for that
explicit canonical form in tests.

The dispatch of `QuantumCircuit.__eq__` to `DAGCircuit.__eq__` means
that direct comparisons of `QuantumCircuit`s will also benefit from this
change.

As a partial implementation detail, the semantic checking is achieved
and defined by the function `expr.structurally_equivalent`, with key
functions for mapping the variables.  The naming discrepancy (semantic
versus structural) is deliberate. Classical expressions are considered
"equal" if and only if their tree structures match exactly; there is no
attempt to move the classical expressions into some canonical form, as
this is not typically simple; even mathematically symmetric operations
typically impact the order of sub-expression evaluation, and we don't
want to get into the weeds of that.  Better to just define equality as
"structurally exactly equal", and have the key function just so
alpha-renaming-compatible variables can still be canonicalised into
something that can be compared between expressions.
  • Loading branch information
jakelishman committed Jul 17, 2023
1 parent 574da7e commit fa3df7a
Show file tree
Hide file tree
Showing 7 changed files with 482 additions and 54 deletions.
11 changes: 10 additions & 1 deletion qiskit/circuit/classical/expr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@
not the general structure, the iterator method :func:`iter_vars` is provided.
.. autofunction:: iter_vars
Two expressions can be compared for direct structural equality by using the built-in Python ``==``
operator. In general, though, one might want to compare two expressions slightly more semantically,
allowing that the :class:`Var` nodes inside them are bound to different memory-location descriptions
between two different circuits. In this case, one can use :func:`structurally_equivalent` with two
suitable "key" functions to do the comparison.
.. autofunction:: structurally_equivalent
"""

__all__ = [
Expand All @@ -153,6 +161,7 @@
"Binary",
"ExprVisitor",
"iter_vars",
"structurally_equivalent",
"lift",
"cast",
"bit_not",
Expand All @@ -172,7 +181,7 @@
]

from .expr import Expr, Var, Value, Cast, Unary, Binary
from .visitors import ExprVisitor, iter_vars
from .visitors import ExprVisitor, iter_vars, structurally_equivalent
from .constructors import (
lift,
cast,
Expand Down
119 changes: 119 additions & 0 deletions qiskit/circuit/classical/expr/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
__all__ = [
"ExprVisitor",
"iter_vars",
"structurally_equivalent",
]

import typing
Expand Down Expand Up @@ -96,3 +97,121 @@ def iter_vars(node: expr.Expr) -> typing.Iterator[expr.Var]:
print(node.var.name)
"""
yield from node.accept(_VAR_WALKER)


class _StructuralEquivalenceImpl(ExprVisitor[bool]):
# The strategy here is to continue to do regular double dispatch through the visitor format,
# since we simply exit out with a ``False`` as soon as the structure of the two trees isn't the
# same; we never need to do any sort of "triple" dispatch. To recurse through both trees
# simultaneously, we hold a pointer to the "full" expression of the other (at the given depth)
# in the stack variables of each visit function, and pass the next "deeper" pointer via the
# `other` state in the class instance.

__slots__ = (
"self_key",
"other_key",
"other",
)

def __init__(self, other: expr.Expr, self_key, other_key):
self.self_key = self_key
self.other_key = other_key
self.other = other

def visit_var(self, node, /):
if self.other.__class__ is not node.__class__ or self.other.type != node.type:
return False
if self.self_key is None or (self_var := self.self_key(node.var)) is None:
self_var = node.var
if self.other_key is None or (other_var := self.other_key(self.other.var)) is None:
other_var = self.other.var
return self_var == other_var

def visit_value(self, node, /):
return (
node.__class__ is self.other.__class__
and node.type == self.other.type
and node.value == self.other.value
)

def visit_unary(self, node, /):
if (
self.other.__class__ is not node.__class__
or self.other.op is not node.op
or self.other.type != node.type
):
return False
self.other = self.other.operand
return node.operand.accept(self)

def visit_binary(self, node, /):
if (
self.other.__class__ is not node.__class__
or self.other.op is not node.op
or self.other.type != node.type
):
return False
other = self.other
self.other = other.left
if not node.left.accept(self):
return False
self.other = other.right
return node.right.accept(self)

def visit_cast(self, node, /):
if self.other.__class__ is not node.__class__ or self.other.type != node.type:
return False
self.other = self.other.operand
return node.operand.accept(self)


def structurally_equivalent(
left: expr.Expr,
right: expr.Expr,
left_var_key: typing.Callable[[typing.Any], typing.Any] | None = None,
right_var_key: typing.Callable[[typing.Any], typing.Any] | None = None,
) -> bool:
"""Do these two expressions have exactly the same tree structure, up to some key function for
the :class:`~.expr.Var` objects?
In other words, are these two expressions the exact same trees, except we compare the
:attr:`.Var.var` fields by calling the appropriate ``*_var_key`` function on them, and comparing
that output for equality. This function does not allow any semantic "equivalences" such as
asserting that ``a == b`` is equivalent to ``b == a``; the evaluation order of the operands
could, in general, cause such a statement to be false (consider hypothetical ``extern``
functions that access global state).
There's no requirements on the key functions, except that their outputs should have general
``__eq__`` methods. If a key function returns ``None``, the variable will be used verbatim
instead.
Args:
left: one of the :class:`~.expr.Expr` nodes.
right: the other :class:`~.expr.Expr` node.
left_var_key: a callable whose output should be used when comparing :attr:`.Var.var`
attributes. If this argument is ``None`` or its output is ``None`` for a given
variable in ``left``, the variable will be used verbatim.
right_var_key: same as ``left_var_key``, but used on the variables in ``right`` instead.
Examples:
Comparing two expressions for structural equivalence, with no remapping of the variables.
These are different because the different :class:`.Clbit` instances compare differently::
>>> from qiskit.circuit import Clbit
>>> from qiskit.circuit.classical import expr
>>> left_bits = [Clbit(), Clbit()]
>>> right_bits = [Clbit(), Clbit()]
>>> left = expr.logic_and(expr.logic_not(left_bits[0]), left_bits[1])
>>> right = expr.logic_and(expr.logic_not(right_bits[0]), right_bits[1])
>>> expr.structurally_equivalent(left, right)
False
Comparing the same two expressions, but this time using mapping functions that associate
the bits with simple indices::
>>> left_key = {var: i for i, var in enumerate(left_bits)}.get
>>> right_key = {var: i for i, var in enumerate(right_bits)}.get
>>> expr.structurally_equivalent(left, right, left_key, right_key)
True
"""
return left.accept(_StructuralEquivalenceImpl(right, left_var_key, right_var_key))
10 changes: 7 additions & 3 deletions qiskit/circuit/controlflow/switch_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,13 @@ def __init__(
def __eq__(self, other):
# The general __eq__ will compare the blocks in the right order, so we just need to ensure
# that all the labels point the right way as well.
return super().__eq__(other) and all(
set(labels_self) == set(labels_other)
for labels_self, labels_other in zip(self._label_spec, other._label_spec)
return (
super().__eq__(other)
and self.target == other.target
and all(
set(labels_self) == set(labels_other)
for labels_self, labels_other in zip(self._label_spec, other._label_spec)
)
)

def cases_specifier(self) -> Iterable[Tuple[Tuple, QuantumCircuit]]:
Expand Down
10 changes: 7 additions & 3 deletions qiskit/converters/circuit_to_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from qiskit.dagcircuit.dagcircuit import DAGCircuit


def circuit_to_dag(circuit, copy_operations=True):
def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None):
"""Build a ``DAGCircuit`` object from a ``QuantumCircuit``.
Args:
Expand All @@ -28,6 +28,10 @@ def circuit_to_dag(circuit, copy_operations=True):
:class:`~.DAGCircuit` will be shared instances and modifications to
operations in the :class:`~.DAGCircuit` will be reflected in the
:class:`~.QuantumCircuit` (and vice versa).
qubit_order (Iterable[Qubit] or None): the ordered that the qubits should be indexed in the
output DAG. Defaults to the same order as in the circuit.
clbit_order (Iterable[Clbit] or None): the ordered that the clbits should be indexed in the
output DAG. Defaults to the same order as in the circuit.
Return:
DAGCircuit: the DAG representing the input circuit.
Expand All @@ -54,8 +58,8 @@ def circuit_to_dag(circuit, copy_operations=True):
dagcircuit.calibrations = circuit.calibrations
dagcircuit.metadata = circuit.metadata

dagcircuit.add_qubits(circuit.qubits)
dagcircuit.add_clbits(circuit.clbits)
dagcircuit.add_qubits(circuit.qubits if qubit_order is None else qubit_order)
dagcircuit.add_clbits(circuit.clbits if clbit_order is None else clbit_order)

for register in circuit.qregs:
dagcircuit.add_qreg(register)
Expand Down
Loading

0 comments on commit fa3df7a

Please sign in to comment.