Skip to content

Commit

Permalink
Add global-phase support to control-flow builders
Browse files Browse the repository at this point in the history
This formalises the concept of global phase within control-flow scopes,
and teaches the control-flow builders to treat modifications to the
global phase as being conditional on the control-flow block being
entered.
  • Loading branch information
jakelishman committed Sep 8, 2023
1 parent c20356b commit c4d4170
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 12 deletions.
4 changes: 3 additions & 1 deletion qiskit/circuit/controlflow/_builder_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ def _unify_circuit_resources_rebuild( # pylint: disable=invalid-name # (it's t
# We use the inner `_append` method because everything is already resolved in the builders.
out_circuits = []
for circuit in circuits:
out = QuantumCircuit(qubits, clbits, *circuit.qregs, *circuit.cregs)
out = QuantumCircuit(
qubits, clbits, *circuit.qregs, *circuit.cregs, global_phase=circuit.global_phase
)
for instruction in circuit.data:
out._append(instruction)
out_circuits.append(out)
Expand Down
7 changes: 6 additions & 1 deletion qiskit/circuit/controlflow/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ class ControlFlowBuilderBlock:
"qubits",
"clbits",
"registers",
"global_phase",
"_allow_jumps",
"_resource_requester",
"_built",
Expand Down Expand Up @@ -254,6 +255,7 @@ def __init__(
self.qubits = set(qubits)
self.clbits = set(clbits)
self.registers = set(registers)
self.global_phase = 0.0
self._allow_jumps = allow_jumps
self._resource_requester = resource_requester
self._built = False
Expand Down Expand Up @@ -417,7 +419,9 @@ def build(

# We start off by only giving the QuantumCircuit the qubits we _know_ it will need, and add
# more later as needed.
out = QuantumCircuit(list(self.qubits), list(self.clbits), *self.registers)
out = QuantumCircuit(
list(self.qubits), list(self.clbits), *self.registers, global_phase=self.global_phase
)

for instruction in self.instructions:
if isinstance(instruction.operation, InstructionPlaceholder):
Expand Down Expand Up @@ -482,6 +486,7 @@ def copy(self) -> "ControlFlowBuilderBlock":
out.qubits = self.qubits.copy()
out.clbits = self.clbits.copy()
out.registers = self.registers.copy()
out.global_phase = self.global_phase
out._allow_jumps = self._allow_jumps
out._forbidden_message = self._forbidden_message
return out
20 changes: 10 additions & 10 deletions qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2435,25 +2435,25 @@ def from_qasm_str(qasm_str: str) -> "QuantumCircuit":

@property
def global_phase(self) -> ParameterValueType:
"""Return the global phase of the circuit in radians."""
"""Return the global phase of the current circuit scope in radians."""
if self._control_flow_scopes:
return self._control_flow_scopes[-1].global_phase
return self._global_phase

@global_phase.setter
def global_phase(self, angle: ParameterValueType):
"""Set the phase of the circuit.
"""Set the phase of the current circuit scope.
Args:
angle (float, ParameterExpression): radians
"""
if isinstance(angle, ParameterExpression) and angle.parameters:
self._global_phase = angle
else:
if not (isinstance(angle, ParameterExpression) and angle.parameters):
# Set the phase to the [0, 2π) interval
angle = float(angle)
if not angle:
self._global_phase = 0
else:
self._global_phase = angle % (2 * np.pi)
angle = float(angle) % (2 * np.pi)
if self._control_flow_scopes:
self._control_flow_scopes[-1].global_phase = angle
else:
self._global_phase = angle

@property
def parameters(self) -> ParameterView:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
fixes:
- |
The control-flow builder interface (the context-manager forms of
:meth:`.QuantumCircuit.if_test`, :meth:`~.QuantumCircuit.while_loop`,
:meth:`~.QuantumCircuit.for_loop` and :meth:`~.QuantumCircuit.switch`) will now correctly track
a separate global-phase advancement within that block. You can add a global-phase advancement
to an inner block by assigning to :attr:`.QuantumCircuit.global_phase` within a builder scope::
from math import pi
from qiskit import QuantumCircuit
qc = QuantumCircuit(3, 3)
qc.global_phase = pi / 2 # Set the outer circuit's global phase.
with qc.if_test((qc.clbits[0], False)) as else_:
# The global phase advancement in a control-flow block begins at 0,
# because it represents how much the phase will be advanced by an
# execution of the block. The defined phase of the outer scope is not
# affected by this set.
qc.global_phase = pi
with else_:
# Similarly, the `else` block may induce a different global-phase
# advancement to the `if`, so it can also be set separately.
qc.global_phase = 1.5 * pi
# The phase advancement caused directly by the outer scope is independent
# of the phase advancement conditionally caused by each control-flow path.
assert qc.global_phase == pi / 2
The meaning of :attr:`.QuantumCircuit.global_phase` is taken to be the global-phase advancement
that is inherent to a single execution of the block. It is still a *global* phase advancement,
in that if the block is entered, the phase of all qubits in the entire program will be advanced.
58 changes: 58 additions & 0 deletions test/python/circuit/test_control_flow_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2940,6 +2940,64 @@ def test_inplace_compose_within_builder(self):

self.assertEqual(canonicalize_control_flow(outer), canonicalize_control_flow(expected))

def test_global_phase_of_blocks(self):
"""It should be possible to set a global phase of a scope independantly of the containing
scope and other sibling scopes."""
qr = QuantumRegister(3)
cr = ClassicalRegister(3)
qc = QuantumCircuit(qr, cr, global_phase=math.pi)

with qc.if_test((qc.clbits[0], False)):
# This scope's phase shouldn't be affected by the outer scope.
self.assertEqual(qc.global_phase, 0.0)
qc.global_phase += math.pi / 2
self.assertEqual(qc.global_phase, math.pi / 2)
# Back outside the scope, the phase shouldn't have changed...
self.assertEqual(qc.global_phase, math.pi)
# ... but we still should be able to see the phase in the built block definition.
self.assertEqual(qc.data[-1].operation.blocks[0].global_phase, math.pi / 2)

with qc.while_loop((qc.clbits[1], False)):
self.assertEqual(qc.global_phase, 0.0)
qc.global_phase = 1 * math.pi / 7
with qc.for_loop(range(3)):
self.assertEqual(qc.global_phase, 0.0)
qc.global_phase = 2 * math.pi / 7

with qc.if_test((qc.clbits[2], False)) as else_:
self.assertEqual(qc.global_phase, 0.0)
qc.global_phase = 3 * math.pi / 7
with else_:
self.assertEqual(qc.global_phase, 0.0)
qc.global_phase = 4 * math.pi / 7

with qc.switch(cr) as case:
with case(0):
self.assertEqual(qc.global_phase, 0.0)
qc.global_phase = 5 * math.pi / 7
with case(case.DEFAULT):
self.assertEqual(qc.global_phase, 0.0)
qc.global_phase = 6 * math.pi / 7

while_body = qc.data[-1].operation.blocks[0]
for_body = while_body.data[0].operation.blocks[0]
if_body, else_body = while_body.data[1].operation.blocks
case_0_body, case_default_body = while_body.data[2].operation.blocks

# The setter should respect exact floating-point equality since the values are in the
# interval [0, pi).
self.assertEqual(
[
while_body.global_phase,
for_body.global_phase,
if_body.global_phase,
else_body.global_phase,
case_0_body.global_phase,
case_default_body.global_phase,
],
[i * math.pi / 7 for i in range(1, 7)],
)


@ddt.ddt
class TestControlFlowBuildersFailurePaths(QiskitTestCase):
Expand Down

0 comments on commit c4d4170

Please sign in to comment.