Skip to content

Commit

Permalink
Support manual variables QuantumCircuit copy methods
Browse files Browse the repository at this point in the history
This commit adds support to the `QuantumCircuit` methods `copy` and
`copy_empty_like` for manual variables.  This involves the non-trivial
extension to the original RFC[^1] that variables can now be
uninitialised; this is somewhat required for the logic of how the
`Store` instruction works and the existence of
`QuantumCircuit.copy_empty_like`; a variable could be initialised with
the result of a `measure` that no longer exists, therefore it must be
possible for variables to be uninitialised.

This was not originally intended to be possible in the design document,
but is somewhat required for logical consistency.  A method
`add_uninitialized_var` is added, so that the behaviour of
`copy_empty_like` is not an awkward special case only possible through
that method, but instead a complete part of the data model that must be
reasoned about.  The method however is deliberately a bit less
ergononmic to type and to use, because really users _should_ use
`add_var` in almost all circumstances.

[^1]: Qiskit/RFCs#50
  • Loading branch information
jakelishman committed Oct 4, 2023
1 parent dd21696 commit be31310
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 0 deletions.
45 changes: 45 additions & 0 deletions qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,36 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V
self._append(CircuitInstruction(store, (), ()))
return var

def add_uninitialized_var(self, var: expr.Var, /):
"""Add a variable with no initializer.
In most cases, you should use :meth:`add_var` to initialize the variable. To use this
function, you must already hold a :class:`~.expr.Var` instance, as the use of the function
typically only makes sense in copying contexts.
.. warning::
Qiskit makes no assertions about what an uninitialized variable will evaluate to at
runtime, and some hardware may reject this as an error.
You should treat this function with caution, and as a low-level primitive that is useful
only in special cases of programmatically rebuilding two like circuits.
Args:
var: the variable to add.
"""
# This function is deliberately meant to be a bit harder to find, to have a long descriptive
# name, and to be a bit less ergonomic than `add_var` (i.e. not allowing the (name, type)
# overload) to discourage people from using it when they should use `add_var`.
#
# This function exists so that there is a method to emulate `copy_empty_like`'s behaviour of
# adding uninitialised variables, which there's no obvious way around. We need to be sure
# that _some_ sort of handling of uninitialised variables is taken into account in our
# structures, so that doesn't become a huge edge case, even though we make no assertions
# about the _meaning_ if such an expression was run on hardware.
var = self._prepare_new_var(var, None)
self._vars_local[var.name] = var

def add_capture(self, var: expr.Var):
"""Add a variable to the circuit that it should capture from a scope it will be contained
within.
Expand Down Expand Up @@ -2465,6 +2495,14 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit":
* global phase
* all the qubits and clbits, including the registers
.. warning::
If the circuit contains any local variable declarations (those added by the
``declarations`` argument to the circuit constructor, or using :meth:`add_var`), they
will be **uninitialized** in the output circuit. You will need to manually add store
instructions for them (see :class:`.Store` and :meth:`.QuantumCircuit.store`) to
initialize them.
Args:
name (str): Name for the copied circuit. If None, then the name stays the same.
Expand All @@ -2485,6 +2523,13 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit":
cpy._qubit_indices = self._qubit_indices.copy()
cpy._clbit_indices = self._clbit_indices.copy()

# Note that this causes the local variables to be uninitialised, because the stores are not
# copied. This can leave the circuit in a potentially dangerous state for users if they
# don't re-add initialiser stores.
cpy._vars_local = self._vars_local.copy()
cpy._vars_input = self._vars_input.copy()
cpy._vars_capture = self._vars_capture.copy()

cpy._parameter_table = ParameterTable()
cpy._data = []

Expand Down
72 changes: 72 additions & 0 deletions test/python/circuit/test_circuit_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from qiskit import BasicAer, ClassicalRegister, QuantumCircuit, QuantumRegister, execute
from qiskit.circuit import Gate, Instruction, Measure, Parameter
from qiskit.circuit.bit import Bit
from qiskit.circuit.classical import expr, types
from qiskit.circuit.classicalregister import Clbit
from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.controlflow import IfElseOp
Expand Down Expand Up @@ -389,6 +390,77 @@ def test_copy_empty_like_circuit(self):
copied = qc.copy_empty_like("copy")
self.assertEqual(copied.name, "copy")

def test_copy_variables(self):
"""Test that a full copy of circuits including variables copies them across."""
a = expr.Var.new("a", types.Bool())
b = expr.Var.new("b", types.Uint(8))
c = expr.Var.new("c", types.Bool())
d = expr.Var.new("d", types.Uint(8))

qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))])
copied = qc.copy()
self.assertEqual({a}, set(copied.iter_input_vars()))
self.assertEqual({c}, set(copied.iter_declared_vars()))
self.assertEqual(
[instruction.operation for instruction in qc],
[instruction.operation for instruction in copied.data],
)

# Check that the original circuit is not mutated.
copied.add_input(b)
copied.add_var(d, 0xFF)
self.assertEqual({a, b}, set(copied.iter_input_vars()))
self.assertEqual({c, d}, set(copied.iter_declared_vars()))
self.assertEqual({a}, set(qc.iter_input_vars()))
self.assertEqual({c}, set(qc.iter_declared_vars()))

qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)])
copied = qc.copy()
self.assertEqual({b}, set(copied.iter_captured_vars()))
self.assertEqual({a, c}, set(copied.iter_declared_vars()))
self.assertEqual(
[instruction.operation for instruction in qc],
[instruction.operation for instruction in copied.data],
)

# Check that the original circuit is not mutated.
copied.add_capture(d)
self.assertEqual({b, d}, set(copied.iter_captured_vars()))
self.assertEqual({b}, set(qc.iter_captured_vars()))

def test_copy_empty_variables(self):
"""Test that an empty copy of circuits including variables copies them across, but does not
initialise them."""
a = expr.Var.new("a", types.Bool())
b = expr.Var.new("b", types.Uint(8))
c = expr.Var.new("c", types.Bool())
d = expr.Var.new("d", types.Uint(8))

qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))])
copied = qc.copy_empty_like()
self.assertEqual({a}, set(copied.iter_input_vars()))
self.assertEqual({c}, set(copied.iter_declared_vars()))
self.assertEqual([], list(copied.data))

# Check that the original circuit is not mutated.
copied.add_input(b)
copied.add_var(d, 0xFF)
self.assertEqual({a, b}, set(copied.iter_input_vars()))
self.assertEqual({c, d}, set(copied.iter_declared_vars()))
self.assertEqual({a}, set(qc.iter_input_vars()))
self.assertEqual({c}, set(qc.iter_declared_vars()))

qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)])
copied = qc.copy_empty_like()
self.assertEqual({b}, set(copied.iter_captured_vars()))
self.assertEqual({a, c}, set(copied.iter_declared_vars()))
self.assertEqual([], list(copied.data))

# Check that the original circuit is not mutated.
copied.add_capture(d)
self.assertEqual({b, d}, set(copied.iter_captured_vars()))
self.assertEqual({b}, set(qc.iter_captured_vars()))

def test_circuit_copy_rejects_invalid_types(self):
"""Test copy method rejects argument with type other than 'string' and 'None' type."""
qc = QuantumCircuit(1, 1)
Expand Down
7 changes: 7 additions & 0 deletions test/python/circuit/test_circuit_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ def test_initialise_captures_declarations(self):
]
self.assertEqual(operations, [("store", b, b_init)])

def test_add_uninitialized_var(self):
a = expr.Var.new("a", types.Bool())
qc = QuantumCircuit()
qc.add_uninitialized_var(a)
self.assertEqual({a}, set(qc.iter_vars()))
self.assertEqual([], list(qc.data))

def test_add_var_returns_good_var(self):
qc = QuantumCircuit()
a = qc.add_var("a", expr.lift(True))
Expand Down

0 comments on commit be31310

Please sign in to comment.