Skip to content

Commit

Permalink
Add representation of storage-owning Var nodes (#10944)
Browse files Browse the repository at this point in the history
* Add representation of storage-owning `Var` nodes

This adds the representation of `expr.Var` nodes that own their own
storage locations, and consequently are not backed by existing Qiskit
objects (`Clbit` or `ClassicalRegister`).  This is the base of the
ability for Qiskit to represent manual classical-value storage in
`QuantumCircuit`, and the base for how manual storage will be
implemented.

* Minor documentation tweaks

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
  • Loading branch information
jakelishman and mtreinish committed Nov 28, 2023
1 parent 7f809a9 commit 50e8137
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 8 deletions.
15 changes: 12 additions & 3 deletions qiskit/circuit/classical/expr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@
These objects are mutable and should not be reused in a different location without a copy.
The entry point from general circuit objects to the expression system is by wrapping the object
in a :class:`Var` node and associating a :class:`~.types.Type` with it.
The base for dynamic variables is the :class:`Var`, which can be either an arbitrarily typed runtime
variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`.
.. autoclass:: Var
:members: var, name
Similarly, literals used in comparison (such as integers) should be lifted to :class:`Value` nodes
with associated types.
Expand Down Expand Up @@ -86,10 +87,18 @@
The functions and methods described in this section are a more user-friendly way to build the
expression tree, while staying close to the internal representation. All these functions will
automatically lift valid Python scalar values into corresponding :class:`Var` or :class:`Value`
objects, and will resolve any required implicit casts on your behalf.
objects, and will resolve any required implicit casts on your behalf. If you want to directly use
some scalar value as an :class:`Expr` node, you can manually :func:`lift` it yourself.
.. autofunction:: lift
Typically you should create memory-owning :class:`Var` instances by using the
:meth:`.QuantumCircuit.add_var` method to declare them in some circuit context, since a
:class:`.QuantumCircuit` will not accept an :class:`Expr` that contains variables that are not
already declared in it, since it needs to know how to allocate the storage and how the variable will
be initialized. However, should you want to do this manually, you should use the low-level
:meth:`Var.new` call to safely generate a named variable for usage.
You can manually specify casts in cases where the cast is allowed in explicit form, but may be
lossy (such as the cast of a higher precision :class:`~.types.Uint` to a lower precision one).
Expand Down
43 changes: 38 additions & 5 deletions qiskit/circuit/classical/expr/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import abc
import enum
import typing
import uuid

from .. import types

Expand Down Expand Up @@ -108,24 +109,56 @@ def __repr__(self):

@typing.final
class Var(Expr):
"""A classical variable."""
"""A classical variable.
__slots__ = ("var",)
These variables take two forms: a new-style variable that owns its storage location and has an
associated name; and an old-style variable that wraps a :class:`.Clbit` or
:class:`.ClassicalRegister` instance that is owned by some containing circuit. In general,
construction of variables for use in programs should use :meth:`Var.new` or
:meth:`.QuantumCircuit.add_var`."""

__slots__ = ("var", "name")

def __init__(
self, var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister, type: types.Type
self,
var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID,
type: types.Type,
*,
name: str | None = None,
):
self.type = type
self.var = var
"""A reference to the backing data storage of the :class:`Var` instance. When lifting
old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`,
this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a
new-style classical variable (one that owns its own storage separate to the old
:class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID`
to uniquely identify it."""
self.name = name
"""The name of the variable. This is required to exist if the backing :attr:`var` attribute
is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is
an old-style variable."""

@classmethod
def new(cls, name: str, type: types.Type) -> typing.Self:
"""Generate a new named variable that owns its own backing storage."""
return cls(uuid.uuid4(), type, name=name)

def accept(self, visitor, /):
return visitor.visit_var(self)

def __eq__(self, other):
return isinstance(other, Var) and self.type == other.type and self.var == other.var
return (
isinstance(other, Var)
and self.type == other.type
and self.var == other.var
and self.name == other.name
)

def __repr__(self):
return f"Var({self.var}, {self.type})"
if self.name is None:
return f"Var({self.var}, {self.type})"
return f"Var({self.var}, {self.type}, name='{self.name}')"


@typing.final
Expand Down
42 changes: 42 additions & 0 deletions test/python/circuit/classical/test_expr_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,45 @@ def test_expr_can_be_cloned(self, obj):
self.assertEqual(obj, copy.copy(obj))
self.assertEqual(obj, copy.deepcopy(obj))
self.assertEqual(obj, pickle.loads(pickle.dumps(obj)))

def test_var_equality(self):
"""Test that various types of :class:`.expr.Var` equality work as expected both in equal and
unequal cases."""
var_a_bool = expr.Var.new("a", types.Bool())
self.assertEqual(var_a_bool, var_a_bool)

# Allocating a new variable should not compare equal, despite the name match. A semantic
# equality checker can choose to key these variables on only their names and types, if it
# knows that that check is valid within the semantic context.
self.assertNotEqual(var_a_bool, expr.Var.new("a", types.Bool()))

# Manually constructing the same object with the same UUID should cause it compare equal,
# though, for serialisation ease.
self.assertEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="a"))

# This is a badly constructed variable because it's using a different type to refer to the
# same storage location (the UUID) as another variable. It is an IR error to generate this
# sort of thing, but we can't fully be responsible for that and a pass would need to go out
# of its way to do this incorrectly, but we can still ensure that the direct equality check
# would spot the error.
self.assertNotEqual(
var_a_bool, expr.Var(var_a_bool.var, types.Uint(8), name=var_a_bool.name)
)

# This is also badly constructed because it uses a different name to refer to the "same"
# storage location.
self.assertNotEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="b"))

# Obviously, two variables of different types and names should compare unequal.
self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(8)))
# As should two variables of the same name but different storage locations and types.
self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("a", types.Uint(8)))

def test_var_uuid_clone(self):
"""Test that :class:`.expr.Var` instances that have an associated UUID and name roundtrip
through pickle and copy operations to produce values that compare equal."""
var_a_u8 = expr.Var.new("a", types.Uint(8))

self.assertEqual(var_a_u8, pickle.loads(pickle.dumps(var_a_u8)))
self.assertEqual(var_a_u8, copy.copy(var_a_u8))
self.assertEqual(var_a_u8, copy.deepcopy(var_a_u8))

0 comments on commit 50e8137

Please sign in to comment.