Skip to content

Commit

Permalink
Add QuantumCircuit.noop for instructionless qubit use
Browse files Browse the repository at this point in the history
This closes a usability gap between the low-level construction of
control-flow ops and the builder interface.  In the builder interface,
there was previously no way to consider a qubit to be "used" by the
scope without adding some corresponding instruction on it.  It was
possible to express this already by manually constructing the blocks.

In general, this is not so useful for the control-flow operations that
we already have present because the additional dependency is spurious
and simply stymies some ability to perform optimisations.  It is also a
fair optimisation to remove the spurious data dependency in the
transpiler.  It becomes more useful with the upcoming `box`, however;
this has additional semantics around its incident data dependencies.
  • Loading branch information
jakelishman committed Jan 31, 2025
1 parent 315d6ad commit dc9e5c9
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 0 deletions.
10 changes: 10 additions & 0 deletions qiskit/circuit/controlflow/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ def get_var(self, name: str) -> Optional[expr.Var]:
the variable if it is found, otherwise ``None``.
"""

@abc.abstractmethod
def use_qubit(self, qubit: Qubit):
"""Called to mark that a :class:`~.circuit.Qubit` should be considered "used" by this scope,
without appending an explicit instruction.
The subclass may assume that the ``qubit`` is valid for the root scope."""


class InstructionResources(typing.NamedTuple):
"""The quantum and classical resources used within a particular instruction.
Expand Down Expand Up @@ -497,6 +504,9 @@ def use_var(self, var: expr.Var):
self._parent.use_var(var)
self._vars_capture[var.name] = var

def use_qubit(self, qubit: Qubit):
self._instructions.add_qubit(qubit, strict=False)

def iter_local_vars(self):
"""Iterator over the variables currently declared in this scope."""
return self._vars_local.values()
Expand Down
45 changes: 45 additions & 0 deletions qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,10 @@ class QuantumCircuit:
``with`` statement. It is far simpler and less error-prone to build control flow
programmatically this way.
When using the control-flow builder interface, you may sometimes want a qubit to be included in
a block, even though it has no operations defined. In this case, you can use the :meth:`noop`
method.
..
TODO: expand the examples of the builder interface.
Expand All @@ -779,6 +783,7 @@ class QuantumCircuit:
.. automethod:: if_test
.. automethod:: switch
.. automethod:: while_loop
.. automethod:: noop
Converting circuits to single objects
Expand Down Expand Up @@ -6176,6 +6181,42 @@ def unitary(

return self.append(gate, qubits, [], copy=False)

def noop(self, *qargs: QubitSpecifier):
"""Mark the given qubit(s) as used within the current scope, without adding an operation.
This has no effect (other than raising an exception on invalid input) when called in the
top scope of a :class:`QuantumCircuit`. Within a control-flow builder, this causes the
qubit to be "used" by the control-flow block, if it wouldn't already be used, without adding
any additional operations on it.
For example::
from qiskit.circuit import QuantumCircuit
qc = QuantumCircuit(3)
with qc.box():
# This control-flow block will only use qubits 0 and 1.
qc.cx(0, 1)
with qc.box():
# This control-flow block will contain only the same operation as the previous
# block, but it will also mark qubit 2 as "used" by the box.
qc.cx(0, 1)
qc.noop(2)
Args:
*qargs: variadic list of valid qubit specifiers. Anything that can be passed as a qubit
or collection of qubits is valid for each argument here.
Raises:
CircuitError: if any requested qubit is not valid for the circuit.
"""
scope = self._current_scope()
for qarg in qargs:
for qubit in self._qbit_argument_conversion(qarg):
# It doesn't matter if we pass duplicates along here, and the inner scope is going
# to have to hash them to check anyway, so no point de-duplicating.
scope.use_qubit(qubit)

def _current_scope(self) -> CircuitScopeInterface:
if self._control_flow_scopes:
return self._control_flow_scopes[-1]
Expand Down Expand Up @@ -6942,6 +6983,10 @@ def use_var(self, var):
if self.get_var(var.name) != var:
raise CircuitError(f"'{var}' is not present in this circuit")

def use_qubit(self, qubit):
# Since the qubit is guaranteed valid, there's nothing for us to do.
pass


def _validate_expr(circuit_scope: CircuitScopeInterface, node: expr.Expr) -> expr.Expr:
# This takes the `circuit_scope` object as an argument rather than being a circuit method and
Expand Down
5 changes: 5 additions & 0 deletions releasenotes/notes/circuit-noop-2ec1f23c0adecb99.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
features_circuits:
- |
A new method, :meth:`.QuantumCircuit.noop`, allows qubits to be marked as explicitly used within
a control-flow builder scope, without adding a corresponding operation to them.
81 changes: 81 additions & 0 deletions test/python/circuit/test_control_flow_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -3494,6 +3494,73 @@ def test_rebuild_captures_variables_in_blocks(self):
]
self.assertEqual(expected, actual)

def test_noop_in_base_scope(self):
base = QuantumCircuit(3)
# Just to check no modifications.
initial_qubits = list(base.qubits)
# No-op on a qubit that's already a no-op.
base.noop(0)
base.cx(0, 1)
# No-op on a qubit that's got a defined operation.
base.noop(base.qubits[1])
# A collection of allowed inputs, where duplicates should be silently ignored.
base.noop(base.qubits, {2}, (1, 0))

expected = QuantumCircuit(3)
expected.cx(0, 1)

self.assertEqual(initial_qubits, base.qubits)
# There should be no impact on the circuit from the no-ops.
self.assertEqual(base, expected)

def test_noop_in_scope(self):
qc = QuantumCircuit([Qubit(), Qubit(), Qubit()], [Clbit()])
# Instruction 0.
with qc.if_test(expr.lift(True)):
qc.noop(0)
# Instruction 1.
with qc.while_loop(expr.lift(False)):
qc.cx(0, 1)
qc.noop(qc.qubits[1])
# Instruction 2.
with qc.for_loop(range(3)):
qc.noop({0}, [1, 0])
qc.x(0)
# Instruction 3.
with qc.switch(expr.lift(3, types.Uint(8))) as case:
with case(0):
qc.noop(0)
with case(1):
qc.noop(1)
# Instruction 4.
with qc.if_test(expr.lift(True)) as else_:
pass
with else_:
with qc.if_test(expr.lift(True)):
qc.noop(2)

expected = QuantumCircuit(qc.qubits, qc.clbits)
body_0 = QuantumCircuit([qc.qubits[0]])
expected.if_test(expr.lift(True), body_0, body_0.qubits, [])
body_1 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
body_1.cx(0, 1)
expected.while_loop(expr.lift(False), body_1, body_1.qubits, [])
body_2 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
body_2.x(0)
expected.for_loop(range(3), None, body_2, body_2.qubits, [])
body_3_0 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
body_3_1 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
expected.switch(
expr.lift(3, types.Uint(8)), [(0, body_3_0), (1, body_3_1)], body_3_0.qubits, []
)
body_4_true = QuantumCircuit([qc.qubits[2]])
body_4_false = QuantumCircuit([qc.qubits[2]])
body_4_false_0 = QuantumCircuit([qc.qubits[2]])
body_4_false.if_test(expr.lift(True), body_4_false_0, body_4_false_0.qubits, [])
expected.if_else(expr.lift(True), body_4_true, body_4_false, body_4_true.qubits, [])

self.assertEqual(qc, expected)


@ddt.ddt
class TestControlFlowBuildersFailurePaths(QiskitTestCase):
Expand Down Expand Up @@ -4158,3 +4225,17 @@ def test_cannot_add_uninitialized_in_scope(self):
with base.for_loop(range(3)):
with self.assertRaisesRegex(CircuitError, "cannot add an uninitialized variable"):
base.add_uninitialized_var(expr.Var.new("a", types.Bool()))

def test_cannot_noop_unknown_qubit(self):
base = QuantumCircuit(2)
# Base scope.
with self.assertRaises(CircuitError):
base.noop(3)
with self.assertRaises(CircuitError):
base.noop(Clbit())
# Control-flow scope.
with base.if_test(expr.lift(True)):
with self.assertRaises(CircuitError):
base.noop(3)
with self.assertRaises(CircuitError):
base.noop(Clbit())

0 comments on commit dc9e5c9

Please sign in to comment.