Skip to content

Commit 104aaca

Browse files
authored
Add QuantumCircuit.noop for instructionless qubit use (#13774)
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.
1 parent a3876f6 commit 104aaca

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

qiskit/circuit/controlflow/builder.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ def get_var(self, name: str) -> Optional[expr.Var]:
166166
the variable if it is found, otherwise ``None``.
167167
"""
168168

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

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

507+
def use_qubit(self, qubit: Qubit):
508+
self._instructions.add_qubit(qubit, strict=False)
509+
500510
def iter_local_vars(self):
501511
"""Iterator over the variables currently declared in this scope."""
502512
return self._vars_local.values()

qiskit/circuit/quantumcircuit.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,10 @@ class QuantumCircuit:
769769
``with`` statement. It is far simpler and less error-prone to build control flow
770770
programmatically this way.
771771
772+
When using the control-flow builder interface, you may sometimes want a qubit to be included in
773+
a block, even though it has no operations defined. In this case, you can use the :meth:`noop`
774+
method.
775+
772776
..
773777
TODO: expand the examples of the builder interface.
774778
@@ -779,6 +783,7 @@ class QuantumCircuit:
779783
.. automethod:: if_test
780784
.. automethod:: switch
781785
.. automethod:: while_loop
786+
.. automethod:: noop
782787
783788
784789
Converting circuits to single objects
@@ -6176,6 +6181,42 @@ def unitary(
61766181

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

6184+
def noop(self, *qargs: QubitSpecifier):
6185+
"""Mark the given qubit(s) as used within the current scope, without adding an operation.
6186+
6187+
This has no effect (other than raising an exception on invalid input) when called in the
6188+
top scope of a :class:`QuantumCircuit`. Within a control-flow builder, this causes the
6189+
qubit to be "used" by the control-flow block, if it wouldn't already be used, without adding
6190+
any additional operations on it.
6191+
6192+
For example::
6193+
6194+
from qiskit.circuit import QuantumCircuit
6195+
6196+
qc = QuantumCircuit(3)
6197+
with qc.box():
6198+
# This control-flow block will only use qubits 0 and 1.
6199+
qc.cx(0, 1)
6200+
with qc.box():
6201+
# This control-flow block will contain only the same operation as the previous
6202+
# block, but it will also mark qubit 2 as "used" by the box.
6203+
qc.cx(0, 1)
6204+
qc.noop(2)
6205+
6206+
Args:
6207+
*qargs: variadic list of valid qubit specifiers. Anything that can be passed as a qubit
6208+
or collection of qubits is valid for each argument here.
6209+
6210+
Raises:
6211+
CircuitError: if any requested qubit is not valid for the circuit.
6212+
"""
6213+
scope = self._current_scope()
6214+
for qarg in qargs:
6215+
for qubit in self._qbit_argument_conversion(qarg):
6216+
# It doesn't matter if we pass duplicates along here, and the inner scope is going
6217+
# to have to hash them to check anyway, so no point de-duplicating.
6218+
scope.use_qubit(qubit)
6219+
61796220
def _current_scope(self) -> CircuitScopeInterface:
61806221
if self._control_flow_scopes:
61816222
return self._control_flow_scopes[-1]
@@ -6942,6 +6983,10 @@ def use_var(self, var):
69426983
if self.get_var(var.name) != var:
69436984
raise CircuitError(f"'{var}' is not present in this circuit")
69446985

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

69466991
def _validate_expr(circuit_scope: CircuitScopeInterface, node: expr.Expr) -> expr.Expr:
69476992
# This takes the `circuit_scope` object as an argument rather than being a circuit method and
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features_circuits:
3+
- |
4+
A new method, :meth:`.QuantumCircuit.noop`, allows qubits to be marked as explicitly used within
5+
a control-flow builder scope, without adding a corresponding operation to them.

test/python/circuit/test_control_flow_builders.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3494,6 +3494,73 @@ def test_rebuild_captures_variables_in_blocks(self):
34943494
]
34953495
self.assertEqual(expected, actual)
34963496

3497+
def test_noop_in_base_scope(self):
3498+
base = QuantumCircuit(3)
3499+
# Just to check no modifications.
3500+
initial_qubits = list(base.qubits)
3501+
# No-op on a qubit that's already a no-op.
3502+
base.noop(0)
3503+
base.cx(0, 1)
3504+
# No-op on a qubit that's got a defined operation.
3505+
base.noop(base.qubits[1])
3506+
# A collection of allowed inputs, where duplicates should be silently ignored.
3507+
base.noop(base.qubits, {2}, (1, 0))
3508+
3509+
expected = QuantumCircuit(3)
3510+
expected.cx(0, 1)
3511+
3512+
self.assertEqual(initial_qubits, base.qubits)
3513+
# There should be no impact on the circuit from the no-ops.
3514+
self.assertEqual(base, expected)
3515+
3516+
def test_noop_in_scope(self):
3517+
qc = QuantumCircuit([Qubit(), Qubit(), Qubit()], [Clbit()])
3518+
# Instruction 0.
3519+
with qc.if_test(expr.lift(True)):
3520+
qc.noop(0)
3521+
# Instruction 1.
3522+
with qc.while_loop(expr.lift(False)):
3523+
qc.cx(0, 1)
3524+
qc.noop(qc.qubits[1])
3525+
# Instruction 2.
3526+
with qc.for_loop(range(3)):
3527+
qc.noop({0}, [1, 0])
3528+
qc.x(0)
3529+
# Instruction 3.
3530+
with qc.switch(expr.lift(3, types.Uint(8))) as case:
3531+
with case(0):
3532+
qc.noop(0)
3533+
with case(1):
3534+
qc.noop(1)
3535+
# Instruction 4.
3536+
with qc.if_test(expr.lift(True)) as else_:
3537+
pass
3538+
with else_:
3539+
with qc.if_test(expr.lift(True)):
3540+
qc.noop(2)
3541+
3542+
expected = QuantumCircuit(qc.qubits, qc.clbits)
3543+
body_0 = QuantumCircuit([qc.qubits[0]])
3544+
expected.if_test(expr.lift(True), body_0, body_0.qubits, [])
3545+
body_1 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
3546+
body_1.cx(0, 1)
3547+
expected.while_loop(expr.lift(False), body_1, body_1.qubits, [])
3548+
body_2 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
3549+
body_2.x(0)
3550+
expected.for_loop(range(3), None, body_2, body_2.qubits, [])
3551+
body_3_0 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
3552+
body_3_1 = QuantumCircuit([qc.qubits[0], qc.qubits[1]])
3553+
expected.switch(
3554+
expr.lift(3, types.Uint(8)), [(0, body_3_0), (1, body_3_1)], body_3_0.qubits, []
3555+
)
3556+
body_4_true = QuantumCircuit([qc.qubits[2]])
3557+
body_4_false = QuantumCircuit([qc.qubits[2]])
3558+
body_4_false_0 = QuantumCircuit([qc.qubits[2]])
3559+
body_4_false.if_test(expr.lift(True), body_4_false_0, body_4_false_0.qubits, [])
3560+
expected.if_else(expr.lift(True), body_4_true, body_4_false, body_4_true.qubits, [])
3561+
3562+
self.assertEqual(qc, expected)
3563+
34973564

34983565
@ddt.ddt
34993566
class TestControlFlowBuildersFailurePaths(QiskitTestCase):
@@ -4158,3 +4225,17 @@ def test_cannot_add_uninitialized_in_scope(self):
41584225
with base.for_loop(range(3)):
41594226
with self.assertRaisesRegex(CircuitError, "cannot add an uninitialized variable"):
41604227
base.add_uninitialized_var(expr.Var.new("a", types.Bool()))
4228+
4229+
def test_cannot_noop_unknown_qubit(self):
4230+
base = QuantumCircuit(2)
4231+
# Base scope.
4232+
with self.assertRaises(CircuitError):
4233+
base.noop(3)
4234+
with self.assertRaises(CircuitError):
4235+
base.noop(Clbit())
4236+
# Control-flow scope.
4237+
with base.if_test(expr.lift(True)):
4238+
with self.assertRaises(CircuitError):
4239+
base.noop(3)
4240+
with self.assertRaises(CircuitError):
4241+
base.noop(Clbit())

0 commit comments

Comments
 (0)