diff --git a/qualtran/_infra/controlled_test.py b/qualtran/_infra/controlled_test.py index 19f52836b..9c3f50532 100644 --- a/qualtran/_infra/controlled_test.py +++ b/qualtran/_infra/controlled_test.py @@ -380,7 +380,7 @@ def test_controlled_tensor_without_decompose(): def test_controlled_global_phase_tensor(): - bloq = GlobalPhase(1.0j).controlled() + bloq = GlobalPhase.from_coefficient(1.0j).controlled() should_be = np.diag([1, 1.0j]) np.testing.assert_allclose(bloq.tensor_contract(), should_be) diff --git a/qualtran/bloqs/basic_gates/global_phase.ipynb b/qualtran/bloqs/basic_gates/global_phase.ipynb index 2427e3f41..e2fac24ab 100644 --- a/qualtran/bloqs/basic_gates/global_phase.ipynb +++ b/qualtran/bloqs/basic_gates/global_phase.ipynb @@ -36,10 +36,17 @@ }, "source": [ "## `GlobalPhase`\n", - "Global phase operation of $z$ (where $|z| = 1$).\n", + "Applies a global phase to the circuit as a whole.\n", + "\n", + "The unitary effect is to multiply the state vector by the complex scalar\n", + "$e^{i pi t}$ for `exponent` $t$.\n", + "\n", + "The global phase of a state or circuit does not affect any observable quantity, but\n", + "keeping track of it can be a useful bookkeeping mechanism for testing circuit identities.\n", + "The global phase becomes important if the gate becomes controlled.\n", "\n", "#### Parameters\n", - " - `coefficient`: a unit complex number $z$ representing the global phase\n", + " - `exponent`: the exponent $t$ of the global phase $e^{i pi t}$ to apply.\n", " - `eps`: precision\n" ] }, @@ -74,7 +81,7 @@ }, "outputs": [], "source": [ - "global_phase = GlobalPhase(1j)" + "global_phase = GlobalPhase(exponent=0.5)" ] }, { @@ -125,6 +132,46 @@ "show_call_graph(global_phase_g)\n", "show_counts_sigma(global_phase_sigma)" ] + }, + { + "cell_type": "markdown", + "id": "c183275c-cc9c-477d-888c-d8b850f67a2e", + "metadata": {}, + "source": [ + "### Tensors and Controlled\n", + "\n", + "The \"tensor\" of the global phase gate is just a number." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d053b20b-3d61-487a-b962-e2368d834c40", + "metadata": {}, + "outputs": [], + "source": [ + "global_phase.tensor_contract()" + ] + }, + { + "cell_type": "markdown", + "id": "b737b871-9c61-4d54-860a-d9928f18808b", + "metadata": {}, + "source": [ + "When a global phase is controlled, it is equivalent to a ZPowGate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d873856e-a687-4b73-acdc-9188dd13a60e", + "metadata": {}, + "outputs": [], + "source": [ + "cgp = global_phase.controlled()\n", + "print(repr(cgp))\n", + "print(cgp.tensor_contract())" + ] } ], "metadata": { @@ -143,7 +190,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.7" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/qualtran/bloqs/basic_gates/global_phase.py b/qualtran/bloqs/basic_gates/global_phase.py index 4c0a87a05..bc3c19178 100644 --- a/qualtran/bloqs/basic_gates/global_phase.py +++ b/qualtran/bloqs/basic_gates/global_phase.py @@ -11,45 +11,73 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from typing import Dict, List, TYPE_CHECKING +from functools import cached_property +from typing import Dict, Iterable, List, Optional, Sequence, Tuple, TYPE_CHECKING import attrs import cirq -from attrs import frozen - -from qualtran import bloq_example, BloqDocSpec, ConnectionT, DecomposeTypeError +from attrs import field, frozen + +from qualtran import ( + AddControlledT, + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + CompositeBloq, + ConnectionT, + CtrlSpec, + DecomposeTypeError, + SoquetT, +) +from qualtran.bloqs.basic_gates.rotation import ZPowGate from qualtran.cirq_interop import CirqGateAsBloqBase from qualtran.cirq_interop.t_complexity_protocol import TComplexity -from qualtran.symbolics import sconj, SymbolicComplex +from qualtran.symbolics import pi, sarg, sexp, SymbolicComplex, SymbolicFloat if TYPE_CHECKING: import quimb.tensor as qtn - from qualtran import CompositeBloq - @frozen class GlobalPhase(CirqGateAsBloqBase): - """Global phase operation of $z$ (where $|z| = 1$). + r"""Applies a global phase to the circuit as a whole. + + The unitary effect is to multiply the state vector by the complex scalar + $e^{i pi t}$ for `exponent` $t$. + + The global phase of a state or circuit does not affect any observable quantity, but + keeping track of it can be a useful bookkeeping mechanism for testing circuit identities. + The global phase becomes important if the gate becomes controlled. Args: - coefficient: a unit complex number $z$ representing the global phase + exponent: the exponent $t$ of the global phase $e^{i pi t}$ to apply. eps: precision """ - coefficient: 'SymbolicComplex' - eps: float = 1e-11 + exponent: SymbolicFloat = field(kw_only=True) + eps: SymbolicFloat = 1e-11 + + @cached_property + def coefficient(self) -> SymbolicComplex: + return sexp(self.exponent * pi(self.exponent) * 1j) + + @classmethod + def from_coefficient( + cls, coefficient: SymbolicComplex, *, eps: SymbolicFloat = 1e-11 + ) -> 'GlobalPhase': + """Applies a global phase of `coefficient`.""" + return cls(exponent=sarg(coefficient) / pi(coefficient), eps=eps) @property def cirq_gate(self) -> cirq.Gate: - return cirq.GlobalPhaseGate(self.coefficient, self.eps) + return cirq.GlobalPhaseGate(self.coefficient) def decompose_bloq(self) -> 'CompositeBloq': raise DecomposeTypeError(f"{self} is atomic") def adjoint(self) -> 'GlobalPhase': - return attrs.evolve(self, coefficient=sconj(self.coefficient)) + return attrs.evolve(self, exponent=-self.exponent) def my_tensors( self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] @@ -58,6 +86,29 @@ def my_tensors( return [qtn.Tensor(data=self.coefficient, inds=[], tags=[str(self)])] + def get_ctrl_system( + self, ctrl_spec: Optional['CtrlSpec'] = None + ) -> Tuple['Bloq', 'AddControlledT']: + + # Delegate to superclass logic for more than one control. + if not (ctrl_spec is None or ctrl_spec == CtrlSpec() or ctrl_spec == CtrlSpec(cvs=0)): + return super().get_ctrl_system(ctrl_spec=ctrl_spec) + + # Otherwise, it's a ZPowGate + if ctrl_spec == CtrlSpec(cvs=0): + bloq = ZPowGate(exponent=-self.exponent, global_shift=-1, eps=self.eps) + else: + bloq = ZPowGate(exponent=self.exponent, eps=self.eps) + + def _add_ctrled( + bb: 'BloqBuilder', ctrl_soqs: Sequence['SoquetT'], in_soqs: Dict[str, 'SoquetT'] + ) -> Tuple[Iterable['SoquetT'], Iterable['SoquetT']]: + (ctrl,) = ctrl_soqs + ctrl = bb.add(bloq, q=ctrl) + return [ctrl], [] + + return bloq, _add_ctrled + def pretty_name(self) -> str: return 'GPhase' @@ -70,12 +121,8 @@ def _t_complexity_(self) -> 'TComplexity': @bloq_example def _global_phase() -> GlobalPhase: - global_phase = GlobalPhase(1j) + global_phase = GlobalPhase(exponent=0.5) return global_phase -_GLOBAL_PHASE_DOC = BloqDocSpec( - bloq_cls=GlobalPhase, - import_line='from qualtran.bloqs.basic_gates import GlobalPhase', - examples=[_global_phase], -) +_GLOBAL_PHASE_DOC = BloqDocSpec(bloq_cls=GlobalPhase, examples=[_global_phase]) diff --git a/qualtran/bloqs/basic_gates/global_phase_test.py b/qualtran/bloqs/basic_gates/global_phase_test.py index 782cca9bd..e23288864 100644 --- a/qualtran/bloqs/basic_gates/global_phase_test.py +++ b/qualtran/bloqs/basic_gates/global_phase_test.py @@ -14,22 +14,48 @@ import cirq import numpy as np +import pytest +from qualtran import CtrlSpec from qualtran.bloqs.basic_gates.global_phase import _global_phase, GlobalPhase +from qualtran.cirq_interop import cirq_gate_to_bloq from qualtran.cirq_interop.t_complexity_protocol import TComplexity def test_unitary(): random_state = np.random.RandomState(2) - for alpha in random_state.random(size=20): + for alpha in random_state.random(size=10): coefficient = np.exp(2j * np.pi * alpha) - bloq = GlobalPhase(coefficient) + bloq = GlobalPhase(exponent=2 * alpha) np.testing.assert_allclose(cirq.unitary(bloq), coefficient) +@pytest.mark.parametrize("cv", [0, 1]) +def test_controlled(cv: int): + ctrl_spec = CtrlSpec(cvs=cv) + random_state = np.random.RandomState(2) + for alpha in random_state.random(size=10): + coefficient = np.exp(2j * np.pi * alpha) + bloq = GlobalPhase(exponent=2 * alpha).controlled(ctrl_spec=ctrl_spec) + np.testing.assert_allclose( + cirq.unitary(cirq.GlobalPhaseGate(coefficient).controlled(control_values=[cv])), + bloq.tensor_contract(), + ) + + +def test_cirq_interop(): + bloq = GlobalPhase.from_coefficient(1.0j) + gate = cirq.GlobalPhaseGate(1.0j) + + circuit = bloq.as_composite_bloq().to_cirq_circuit() + assert cirq.approx_eq(circuit, cirq.Circuit(gate.on()), atol=1e-16) + + assert cirq_gate_to_bloq(gate) == bloq + + def test_t_complexity(): - assert GlobalPhase(1j).t_complexity() == TComplexity() + assert GlobalPhase(exponent=0.5).t_complexity() == TComplexity() def test_global_phase(bloq_autotester): diff --git a/qualtran/bloqs/basic_gates/rotation.py b/qualtran/bloqs/basic_gates/rotation.py index ac40d416f..44901a0b7 100644 --- a/qualtran/bloqs/basic_gates/rotation.py +++ b/qualtran/bloqs/basic_gates/rotation.py @@ -77,14 +77,16 @@ class ZPowGate(CirqGateAsBloqBase): """ exponent: SymbolicFloat = 1.0 - global_shift: float = 0.0 - eps: float = 1e-11 + global_shift: SymbolicFloat = 0.0 + eps: SymbolicFloat = 1e-11 def decompose_bloq(self) -> 'CompositeBloq': raise DecomposeTypeError(f"{self} is atomic") @cached_property def cirq_gate(self) -> cirq.Gate: + if isinstance(self.global_shift, sympy.Expr): + raise TypeError(f"cirq.ZPowGate does not support symbolic {self.global_shift=}") return cirq.ZPowGate(exponent=self.exponent, global_shift=self.global_shift) def __pow__(self, power): diff --git a/qualtran/bloqs/basic_gates/su2_rotation.py b/qualtran/bloqs/basic_gates/su2_rotation.py index 3e7e2f51f..071f75814 100644 --- a/qualtran/bloqs/basic_gates/su2_rotation.py +++ b/qualtran/bloqs/basic_gates/su2_rotation.py @@ -23,7 +23,7 @@ from qualtran.bloqs.basic_gates import GlobalPhase, Ry, ZPowGate from qualtran.cirq_interop.t_complexity_protocol import TComplexity from qualtran.drawing import Text, TextBox -from qualtran.symbolics import is_symbolic, SymbolicFloat +from qualtran.symbolics import is_symbolic, pi, SymbolicFloat if TYPE_CHECKING: import quimb.tensor as qtn @@ -125,13 +125,17 @@ def _unitary_(self): return self.rotation_matrix def build_composite_bloq(self, bb: 'BloqBuilder', q: 'SoquetT') -> Dict[str, 'SoquetT']: - pi = sympy.pi if self.is_symbolic() else np.pi - exp = sympy.exp if self.is_symbolic() else np.exp - - bb.add(GlobalPhase(coefficient=-exp(1j * self.global_shift), eps=self.eps / 4)) - q = bb.add(ZPowGate(exponent=1 - self.lambd / pi, global_shift=-1, eps=self.eps / 4), q=q) + bb.add( + GlobalPhase(exponent=1 + self.global_shift / pi(self.global_shift), eps=self.eps / 4) + ) + q = bb.add( + ZPowGate(exponent=1 - self.lambd / pi(self.lambd), global_shift=-1, eps=self.eps / 4), + q=q, + ) q = bb.add(Ry(angle=2 * self.theta, eps=self.eps / 4), q=q) - q = bb.add(ZPowGate(exponent=-self.phi / pi, global_shift=-1, eps=self.eps / 4), q=q) + q = bb.add( + ZPowGate(exponent=-self.phi / pi(self.phi), global_shift=-1, eps=self.eps / 4), q=q + ) return {'q': q} def adjoint(self) -> 'SU2RotationGate': diff --git a/qualtran/bloqs/basic_gates/su2_rotation_test.py b/qualtran/bloqs/basic_gates/su2_rotation_test.py index bb89fba8f..9c518e761 100644 --- a/qualtran/bloqs/basic_gates/su2_rotation_test.py +++ b/qualtran/bloqs/basic_gates/su2_rotation_test.py @@ -99,7 +99,7 @@ def test_call_graph(): gate = SU2RotationGate(theta, phi, lambd, alpha, eps) _, sigma = gate.call_graph() assert sigma == { - GlobalPhase(-sympy.exp(1j * alpha), eps / 4): 1, + GlobalPhase(exponent=1 + alpha / pi, eps=eps / 4): 1, ZPowGate(-phi / pi, -1, eps / 4): 1, ZPowGate(-lambd / pi + 1, -1, eps / 4): 1, Ry(2 * theta, eps / 4): 1, diff --git a/qualtran/bloqs/phase_estimation/lp_resource_state.py b/qualtran/bloqs/phase_estimation/lp_resource_state.py index 2d2fb737d..d657d4f64 100644 --- a/qualtran/bloqs/phase_estimation/lp_resource_state.py +++ b/qualtran/bloqs/phase_estimation/lp_resource_state.py @@ -159,7 +159,7 @@ def decompose_from_registers( # Reset ancilla to |0> state. yield [XGate().on(flag), XGate().on(anc)] - yield GlobalPhase(1j).on() + yield GlobalPhase(exponent=0.5).on() context.qubit_manager.qfree([flag, anc]) def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: @@ -173,7 +173,7 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: (Ry(angle=flag_angle), 3), (MultiControlPauli(cvs, target_gate=cirq.Z), 1), (XGate(), 4), - (GlobalPhase(1j), 1), + (GlobalPhase(exponent=0.5), 1), (CZPowGate(), 1), } diff --git a/qualtran/bloqs/reflections/reflection_using_prepare.py b/qualtran/bloqs/reflections/reflection_using_prepare.py index 9dabf41f5..13e02231d 100644 --- a/qualtran/bloqs/reflections/reflection_using_prepare.py +++ b/qualtran/bloqs/reflections/reflection_using_prepare.py @@ -20,14 +20,13 @@ import numpy as np from numpy.typing import NDArray -from qualtran import bloq_example, BloqDocSpec, QBit, Register, Signature +from qualtran import Bloq, bloq_example, BloqDocSpec, CtrlSpec, QBit, Register, Signature from qualtran._infra.gate_with_registers import ( merge_qubits, SpecializedSingleQubitControlledGate, total_bits, ) from qualtran.bloqs.basic_gates.global_phase import GlobalPhase -from qualtran.bloqs.basic_gates.rotation import ZPowGate from qualtran.bloqs.basic_gates.x_basis import XGate from qualtran.bloqs.mcmt.multi_control_multi_target_pauli import MultiControlPauli from qualtran.resource_counting.generalizers import ignore_split_join @@ -176,12 +175,10 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: if self.control_val is None: costs.add((XGate(), 2)) if self.global_phase != 1: - if self.control_val is None: - costs.add((GlobalPhase(self.global_phase, self.eps), 1)) - else: - costs.add( - (ZPowGate(exponent=float(np.angle(self.global_phase)) / np.pi, eps=self.eps), 1) - ) + phase_op: Bloq = GlobalPhase.from_coefficient(self.global_phase, eps=self.eps) + if self.control_val is not None: + phase_op = phase_op.controlled(ctrl_spec=CtrlSpec(cvs=self.control_val)) + costs.add((phase_op, 1)) return costs diff --git a/qualtran/bloqs/reflections/reflection_using_prepare_test.py b/qualtran/bloqs/reflections/reflection_using_prepare_test.py index 9aac402c4..a88744e34 100644 --- a/qualtran/bloqs/reflections/reflection_using_prepare_test.py +++ b/qualtran/bloqs/reflections/reflection_using_prepare_test.py @@ -271,9 +271,7 @@ def test_call_graph_matches_decomp(global_phase, control_val): def catch_zpow_bloq_s_gate_inv(bloq) -> Optional[Bloq]: # Hack to catch the fact that cirq special cases some ZPowGates - if isinstance(bloq, ZPowGate) and np.isclose( - float(bloq.exponent), np.angle(global_phase) / np.pi - ): + if isinstance(bloq, ZPowGate) and np.any(np.isclose(float(bloq.exponent), [0.5, -0.5])): # we're already ignoring cliffords return None return bloq @@ -281,15 +279,15 @@ def catch_zpow_bloq_s_gate_inv(bloq) -> Optional[Bloq]: gate = ReflectionUsingPrepare( prepare_gate, global_phase=global_phase, eps=eps, control_val=control_val ) - cost_decomp = gate.decompose_bloq().call_graph( + _, cost_decomp = gate.decompose_bloq().call_graph( generalizer=[ignore_split_join, ignore_alloc_free, ignore_cliffords] - )[1] - cost_call = gate.call_graph( + ) + _, cost_call = gate.call_graph( generalizer=[ ignore_split_join, ignore_alloc_free, ignore_cliffords, catch_zpow_bloq_s_gate_inv, ] - )[1] + ) assert cost_decomp == cost_call diff --git a/qualtran/cirq_interop/_cirq_to_bloq.py b/qualtran/cirq_interop/_cirq_to_bloq.py index 3acf9a9e3..e14da41ec 100644 --- a/qualtran/cirq_interop/_cirq_to_bloq.py +++ b/qualtran/cirq_interop/_cirq_to_bloq.py @@ -403,8 +403,8 @@ def cirq_gate_to_bloq(gate: cirq.Gate) -> Bloq: if isinstance(gate, cirq.GlobalPhaseGate): if isinstance(gate.coefficient, numbers.Complex): - return GlobalPhase(coefficient=complex(gate.coefficient)) - return GlobalPhase(coefficient=gate.coefficient) + return GlobalPhase.from_coefficient(coefficient=complex(gate.coefficient)) + return GlobalPhase.from_coefficient(coefficient=gate.coefficient) # No known basic gate, wrap the cirq gate in a CirqGateAsBloq wrapper. return CirqGateAsBloq(gate) diff --git a/qualtran/cirq_interop/_cirq_to_bloq_test.py b/qualtran/cirq_interop/_cirq_to_bloq_test.py index c4b62fd31..31b1b6554 100644 --- a/qualtran/cirq_interop/_cirq_to_bloq_test.py +++ b/qualtran/cirq_interop/_cirq_to_bloq_test.py @@ -228,4 +228,4 @@ def test_cirq_gate_as_bloq_decompose_raises(): def test_cirq_gate_as_bloq_diagram_info(): - assert cirq.circuit_diagram_info(GlobalPhase(1j)) is None + assert cirq.circuit_diagram_info(GlobalPhase(exponent=0.5)) is None diff --git a/qualtran/symbolics/__init__.py b/qualtran/symbolics/__init__.py index ef0b181f3..8e84f066e 100644 --- a/qualtran/symbolics/__init__.py +++ b/qualtran/symbolics/__init__.py @@ -22,7 +22,9 @@ pi, prod, sabs, + sarg, sconj, + sexp, shape, slen, smax, diff --git a/qualtran/symbolics/math_funcs.py b/qualtran/symbolics/math_funcs.py index 2add3071c..fefa8c37a 100644 --- a/qualtran/symbolics/math_funcs.py +++ b/qualtran/symbolics/math_funcs.py @@ -38,6 +38,19 @@ def log2(x: SymbolicFloat) -> SymbolicFloat: return log2(x) +def sexp(x: SymbolicComplex) -> SymbolicComplex: + if isinstance(x, sympy.Basic): + return sympy.exp(x) + return np.exp(x) + + +def sarg(x: SymbolicComplex) -> SymbolicFloat: + r"""Argument $t$ of a complex number $r e^{i t}$""" + if isinstance(x, sympy.Basic): + return sympy.arg(x) + return float(np.angle(x)) + + def sabs(x: SymbolicFloat) -> SymbolicFloat: return cast(SymbolicFloat, abs(x)) diff --git a/qualtran/symbolics/math_funcs_test.py b/qualtran/symbolics/math_funcs_test.py index 69a406937..9aad7a0f8 100644 --- a/qualtran/symbolics/math_funcs_test.py +++ b/qualtran/symbolics/math_funcs_test.py @@ -16,7 +16,18 @@ import sympy from sympy.codegen.cfunctions import log2 as sympy_log2 -from qualtran.symbolics import bit_length, ceil, is_symbolic, log2, Shaped, slen, smax, smin +from qualtran.symbolics import ( + bit_length, + ceil, + is_symbolic, + log2, + sarg, + sexp, + Shaped, + slen, + smax, + smin, +) def test_log2(): @@ -25,6 +36,21 @@ def test_log2(): assert log2(10) == np.log2(10) +def test_sexp(): + x = sympy.Symbol('x') + assert sexp(4) == np.exp(4) + assert sexp(x) == sympy.exp(x) + assert sexp(sympy.pi * sympy.I / 2) == sympy.I + + +def test_sarg(): + z = sympy.Symbol('z') + assert sarg(4) == 0 + assert sarg(1j) == np.pi / 2 + assert sarg(z) == sympy.arg(z) + assert sarg(sympy.I) == sympy.pi / 2 + + def test_ceil(): assert ceil(sympy.Symbol('x')) == sympy.ceiling(sympy.Symbol('x')) assert ceil(sympy.Number(10.123)) == sympy.ceiling(sympy.Number(11))