diff --git a/qiskit/circuit/controlflow/_builder_utils.py b/qiskit/circuit/controlflow/_builder_utils.py index b78079df7ae2..5ba5c9612c9d 100644 --- a/qiskit/circuit/controlflow/_builder_utils.py +++ b/qiskit/circuit/controlflow/_builder_utils.py @@ -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) diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index db6d93ade441..c8ada706e5fe 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -203,6 +203,7 @@ class ControlFlowBuilderBlock: "qubits", "clbits", "registers", + "global_phase", "_allow_jumps", "_resource_requester", "_built", @@ -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 @@ -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): @@ -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 diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 651cde25678e..4dffaf55cfbc 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -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: diff --git a/releasenotes/notes/fix-global-phase-control-flow-builders-38302f41302be928.yaml b/releasenotes/notes/fix-global-phase-control-flow-builders-38302f41302be928.yaml new file mode 100644 index 000000000000..47ef7f60f6f9 --- /dev/null +++ b/releasenotes/notes/fix-global-phase-control-flow-builders-38302f41302be928.yaml @@ -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. diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 35db299c2fd6..aa4974f4cdec 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -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):