diff --git a/cirq/__init__.py b/cirq/__init__.py index 41f7686a3c4..7853ab7c3dd 100644 --- a/cirq/__init__.py +++ b/cirq/__init__.py @@ -234,6 +234,7 @@ PhaseGradientGate, PhasedISwapPowGate, PhasedXPowGate, + PhasedXZGate, PhaseFlipChannel, QFT, Qid, diff --git a/cirq/ops/__init__.py b/cirq/ops/__init__.py index 3c954ffd36d..be15594e73f 100644 --- a/cirq/ops/__init__.py +++ b/cirq/ops/__init__.py @@ -196,6 +196,9 @@ from cirq.ops.phased_x_gate import ( PhasedXPowGate,) +from cirq.ops.phased_x_z_gate import ( + PhasedXZGate,) + from cirq.ops.raw_types import ( Gate, Operation, diff --git a/cirq/ops/phased_x_z_gate.py b/cirq/ops/phased_x_z_gate.py new file mode 100644 index 00000000000..4c4d510c2fb --- /dev/null +++ b/cirq/ops/phased_x_z_gate.py @@ -0,0 +1,184 @@ +import numbers +from typing import Union, TYPE_CHECKING, Tuple, Optional, Sequence + +import numpy as np +import sympy + +from cirq import value, ops, protocols, linalg +from cirq.ops import gate_features +from cirq._compat import proper_repr + +if TYPE_CHECKING: + import cirq + + +@value.value_equality(approximate=True) +class PhasedXZGate(gate_features.SingleQubitGate): + """A single qubit operation expressed as $Z^z Z^a X^x Z^{-a}$. + + The above expression is a matrix multiplication with time going to the left. + In quantum circuit notation, this operation decomposes into this circuit: + + ───Z^(-a)──X^x──Z^a────Z^z───$ + + The axis phase exponent (a) decides which axis in the XY plane to rotate + around. The amount of rotation around that axis is decided by the x + exponent (x). Then the z exponent (z) decides how much to phase the qubit. + """ + + def __init__(self, *, x_exponent: Union[numbers.Real, sympy.Basic], + z_exponent: Union[numbers.Real, sympy.Basic], + axis_phase_exponent: Union[numbers.Real, sympy.Basic]) -> None: + """ + Args: + x_exponent: Determines how much to rotate during the + axis-in-XY-plane rotation. The $x$ in $Z^z Z^a X^x Z^{-a}$. + z_exponent: The amount of phasing to apply after the + axis-in-XY-plane rotation. The $z$ in $Z^z Z^a X^x Z^{-a}$. + axis_phase_exponent: Determines which axis to rotate around during + the axis-in-XY-plane rotation. The $a$ in $Z^z Z^a X^x Z^{-a}$. + """ + self._x_exponent = x_exponent + self._z_exponent = z_exponent + self._axis_phase_exponent = axis_phase_exponent + + def _canonical(self) -> 'cirq.PhasedXZGate': + x = self.x_exponent + z = self.z_exponent + a = self.axis_phase_exponent + + # Canonicalize X exponent into (-1, +1]. + if isinstance(x, numbers.Real): + x %= 2 + if x > 1: + x -= 2 + # Axis phase exponent is irrelevant if there is no X exponent. + if x == 0: + a = 0 + # For 180 degree X rotations, the axis phase and z exponent overlap. + if x == 1 and z != 0: + a -= z / 2 + z = 0 + + # Canonicalize Z exponent into (-1, +1]. + if isinstance(z, numbers.Real): + z %= 2 + if z > 1: + z -= 2 + + # Canonicalize axis phase exponent into (-0.5, +0.5]. + if isinstance(a, numbers.Real): + a %= 2 + if a > 1: + a -= 2 + if a <= -0.5: + a += 1 + x = -x + elif a > 0.5: + a -= 1 + x = -x + + return PhasedXZGate(x_exponent=x, z_exponent=z, axis_phase_exponent=a) + + @property + def x_exponent(self) -> Union[numbers.Real, sympy.Basic]: + return self._x_exponent + + @property + def z_exponent(self) -> Union[numbers.Real, sympy.Basic]: + return self._z_exponent + + @property + def axis_phase_exponent(self) -> Union[numbers.Real, sympy.Basic]: + return self._axis_phase_exponent + + def _value_equality_values_(self): + c = self._canonical() + return ( + value.PeriodicValue(c._x_exponent, 2), + value.PeriodicValue(c._z_exponent, 2), + value.PeriodicValue(c._axis_phase_exponent, 2), + ) + + @staticmethod + def from_matrix(mat: np.array) -> 'cirq.PhasedXZGate': + pre_phase, rotation, post_phase = ( + linalg.deconstruct_single_qubit_matrix_into_angles(mat)) + pre_phase /= np.pi + post_phase /= np.pi + rotation /= np.pi + pre_phase -= 0.5 + post_phase += 0.5 + return PhasedXZGate(x_exponent=rotation, + axis_phase_exponent=-pre_phase, + z_exponent=post_phase + pre_phase)._canonical() + + def _qasm_(self, args: 'cirq.QasmArgs', + qubits: Tuple['cirq.Qid', ...]) -> Optional[str]: + from cirq.circuits import qasm_output + qasm_gate = qasm_output.QasmUGate(lmda=0.5 - self._axis_phase_exponent, + theta=self._x_exponent, + phi=self._z_exponent + + self._axis_phase_exponent - 0.5) + return protocols.qasm(qasm_gate, args=args, qubits=qubits) + + def _decompose_(self, qubits: Sequence['cirq.Qid']) -> 'cirq.OP_TREE': + q = qubits[0] + yield ops.Z(q)**-self._axis_phase_exponent + yield ops.X(q)**self._x_exponent + yield ops.Z(q)**(self._axis_phase_exponent + self._z_exponent) + + def __pow__(self, exponent: Union[float, int]) -> 'PhasedXZGate': + if exponent == 1: + return self + if exponent == -1: + return PhasedXZGate( + x_exponent=-self._x_exponent, + z_exponent=-self._z_exponent, + axis_phase_exponent=self._z_exponent + self.axis_phase_exponent, + ) + return NotImplemented + + def _is_parameterized_(self) -> bool: + """See `cirq.SupportsParameterization`.""" + return (protocols.is_parameterized(self._x_exponent) or + protocols.is_parameterized(self._z_exponent) or + protocols.is_parameterized(self._axis_phase_exponent)) + + def _resolve_parameters_(self, param_resolver) -> 'cirq.PhasedXZGate': + """See `cirq.SupportsParameterization`.""" + return PhasedXZGate( + z_exponent=param_resolver.value_of(self._z_exponent), + x_exponent=param_resolver.value_of(self._x_exponent), + axis_phase_exponent=param_resolver.value_of( + self._axis_phase_exponent), + ) + + def _phase_by_(self, phase_turns, qubit_index) -> 'cirq.PhasedXZGate': + """See `cirq.SupportsPhase`.""" + assert qubit_index == 0 + return PhasedXZGate(x_exponent=self._x_exponent, + z_exponent=self._z_exponent, + axis_phase_exponent=self._axis_phase_exponent + + phase_turns * 2) + + def _circuit_diagram_info_(self, + args: 'cirq.CircuitDiagramInfoArgs') -> str: + """See `cirq.SupportsCircuitDiagramInfo`.""" + return (f'PhXZ(' + f'a={args.format_real(self._axis_phase_exponent)},' + f'x={args.format_real(self._x_exponent)},' + f'z={args.format_real(self._z_exponent)})') + + def __str__(self): + return protocols.circuit_diagram_info(self).wire_symbols[0] + + def __repr__(self): + return (f'cirq.PhasedXZGate(' + f'axis_phase_exponent={proper_repr(self._axis_phase_exponent)},' + f' x_exponent={proper_repr(self._x_exponent)}, ' + f'z_exponent={proper_repr(self._z_exponent)})') + + def _json_dict_(self): + return protocols.obj_to_dict_helper( + self, ['axis_phase_exponent', 'x_exponent', 'z_exponent']) diff --git a/cirq/ops/phased_x_z_gate_test.py b/cirq/ops/phased_x_z_gate_test.py new file mode 100644 index 00000000000..2c10aca5f31 --- /dev/null +++ b/cirq/ops/phased_x_z_gate_test.py @@ -0,0 +1,268 @@ +import random + +import numpy as np +import sympy + +import cirq + + +def test_init_properties(): + g = cirq.PhasedXZGate(x_exponent=0.125, + z_exponent=0.25, + axis_phase_exponent=0.375) + assert g.x_exponent == 0.125 + assert g.z_exponent == 0.25 + assert g.axis_phase_exponent == 0.375 + + +def test_eq(): + eq = cirq.testing.EqualsTester() + eq.make_equality_group(lambda: cirq.PhasedXZGate( + x_exponent=0.25, z_exponent=0.5, axis_phase_exponent=0.75)) + + # Sensitive to each parameter. + eq.add_equality_group( + cirq.PhasedXZGate(x_exponent=0, + z_exponent=0.5, + axis_phase_exponent=0.75)) + eq.add_equality_group( + cirq.PhasedXZGate(x_exponent=0.25, + z_exponent=0, + axis_phase_exponent=0.75)) + eq.add_equality_group( + cirq.PhasedXZGate(x_exponent=0.25, + z_exponent=0.5, + axis_phase_exponent=0)) + + # Different from other gates. + eq.add_equality_group( + cirq.PhasedXPowGate(exponent=0.25, phase_exponent=0.75)) + eq.add_equality_group(cirq.X) + eq.add_equality_group( + cirq.PhasedXZGate(x_exponent=1, z_exponent=0, axis_phase_exponent=0)) + + +def test_canonicalization(): + + def f(x, z, a): + return cirq.PhasedXZGate(x_exponent=x, + z_exponent=z, + axis_phase_exponent=a) + + # Canonicalizations are equivalent. + eq = cirq.testing.EqualsTester() + eq.add_equality_group(f(-1, 0, 0), f(-3, 0, 0), f(1, 1, 0.5)) + """ + # Canonicalize X exponent (-1, +1]. + if isinstance(x, numbers.Real): + x %= 2 + if x > 1: + x -= 2 + # Axis phase exponent is irrelevant if there is no X exponent. + # Canonicalize Z exponent (-1, +1]. + if isinstance(z, numbers.Real): + z %= 2 + if z > 1: + z -= 2 + + # Canonicalize axis phase exponent into (-0.5, +0.5]. + if isinstance(a, numbers.Real): + a %= 2 + if a > 1: + a -= 2 + if a <= -0.5: + a += 1 + x = -x + elif a > 0.5: + a -= 1 + x = -x + """ + + # X rotation gets canonicalized. + t = f(3, 0, 0)._canonical() + assert t.x_exponent == 1 + assert t.z_exponent == 0 + assert t.axis_phase_exponent == 0 + t = f(1.5, 0, 0)._canonical() + assert t.x_exponent == -0.5 + assert t.z_exponent == 0 + assert t.axis_phase_exponent == 0 + + # Z rotation gets canonicalized. + t = f(0, 3, 0)._canonical() + assert t.x_exponent == 0 + assert t.z_exponent == 1 + assert t.axis_phase_exponent == 0 + t = f(0, 1.5, 0)._canonical() + assert t.x_exponent == 0 + assert t.z_exponent == -0.5 + assert t.axis_phase_exponent == 0 + + # Axis phase gets canonicalized. + t = f(0.5, 0, 2.25)._canonical() + assert t.x_exponent == 0.5 + assert t.z_exponent == 0 + assert t.axis_phase_exponent == 0.25 + t = f(0.5, 0, 1.25)._canonical() + assert t.x_exponent == -0.5 + assert t.z_exponent == 0 + assert t.axis_phase_exponent == 0.25 + t = f(0.5, 0, 0.75)._canonical() + assert t.x_exponent == -0.5 + assert t.z_exponent == 0 + assert t.axis_phase_exponent == -0.25 + + # 180 degree rotations don't need a virtual Z. + t = f(1, 1, 0.5)._canonical() + assert t.x_exponent == 1 + assert t.z_exponent == 0 + assert t.axis_phase_exponent == 0 + t = f(1, 0.25, 0.5)._canonical() + assert t.x_exponent == 1 + assert t.z_exponent == 0 + assert t.axis_phase_exponent == 0.375 + + # Axis phase is irrelevant when not rotating. + t = f(0, 0.25, 0.5)._canonical() + assert t.x_exponent == 0 + assert t.z_exponent == 0.25 + assert t.axis_phase_exponent == 0 + + +def test_from_matrix(): + # Axis rotations. + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix( + cirq.unitary(cirq.X**0.1)), + cirq.PhasedXZGate(x_exponent=0.1, + z_exponent=0, + axis_phase_exponent=0), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix( + cirq.unitary(cirq.X**-0.1)), + cirq.PhasedXZGate(x_exponent=-0.1, + z_exponent=0, + axis_phase_exponent=0), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix( + cirq.unitary(cirq.Y**0.1)), + cirq.PhasedXZGate(x_exponent=0.1, + z_exponent=0, + axis_phase_exponent=0.5), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix( + cirq.unitary(cirq.Y**-0.1)), + cirq.PhasedXZGate(x_exponent=-0.1, + z_exponent=0, + axis_phase_exponent=0.5), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix( + cirq.unitary(cirq.Z**-0.1)), + cirq.PhasedXZGate(x_exponent=0, + z_exponent=-0.1, + axis_phase_exponent=0), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix( + cirq.unitary(cirq.Z**0.1)), + cirq.PhasedXZGate(x_exponent=0, + z_exponent=0.1, + axis_phase_exponent=0), + atol=1e-8) + + # Pauli matrices. + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix(np.eye(2)), + cirq.PhasedXZGate(x_exponent=0, + z_exponent=0, + axis_phase_exponent=0), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix(cirq.unitary(cirq.X)), + cirq.PhasedXZGate(x_exponent=1, + z_exponent=0, + axis_phase_exponent=0), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix(cirq.unitary(cirq.Y)), + cirq.PhasedXZGate(x_exponent=1, + z_exponent=0, + axis_phase_exponent=0.5), + atol=1e-8) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix(cirq.unitary(cirq.Z)), + cirq.PhasedXZGate(x_exponent=0, + z_exponent=1, + axis_phase_exponent=0), + atol=1e-8) + + # Round trips. + a = random.random() + b = random.random() + c = random.random() + g = cirq.PhasedXZGate(x_exponent=a, z_exponent=b, axis_phase_exponent=c) + assert cirq.approx_eq(cirq.PhasedXZGate.from_matrix(cirq.unitary(g)), + g, + atol=1e-8) + u = cirq.testing.random_unitary(2) + cirq.testing.assert_allclose_up_to_global_phase(cirq.unitary( + cirq.PhasedXZGate.from_matrix(u)), + u, + atol=1e-8) + + +def test_protocols(): + a = random.random() + b = random.random() + c = random.random() + g = cirq.PhasedXZGate(x_exponent=a, z_exponent=b, axis_phase_exponent=c) + cirq.testing.assert_implements_consistent_protocols(g) + + # Symbolic. + t = sympy.Symbol('t') + g = cirq.PhasedXZGate(x_exponent=t, z_exponent=b, axis_phase_exponent=c) + cirq.testing.assert_implements_consistent_protocols(g) + g = cirq.PhasedXZGate(x_exponent=a, z_exponent=t, axis_phase_exponent=c) + cirq.testing.assert_implements_consistent_protocols(g) + g = cirq.PhasedXZGate(x_exponent=a, z_exponent=b, axis_phase_exponent=t) + cirq.testing.assert_implements_consistent_protocols(g) + + +def test_inverse(): + a = random.random() + b = random.random() + c = random.random() + q = cirq.LineQubit(0) + g = cirq.PhasedXZGate(x_exponent=a, z_exponent=b, + axis_phase_exponent=c).on(q) + + cirq.testing.assert_allclose_up_to_global_phase( + cirq.unitary(g**-1), + np.transpose(np.conjugate(cirq.unitary(g))), + atol=1e-8) + + +def test_parameterized(): + a = random.random() + b = random.random() + c = random.random() + g = cirq.PhasedXZGate(x_exponent=a, z_exponent=b, axis_phase_exponent=c) + assert not cirq.is_parameterized(g) + + t = sympy.Symbol('t') + gt = cirq.PhasedXZGate(x_exponent=t, z_exponent=b, axis_phase_exponent=c) + assert cirq.is_parameterized(gt) + assert cirq.resolve_parameters(gt, {'t': a}) == g + gt = cirq.PhasedXZGate(x_exponent=a, z_exponent=t, axis_phase_exponent=c) + assert cirq.is_parameterized(gt) + assert cirq.resolve_parameters(gt, {'t': b}) == g + gt = cirq.PhasedXZGate(x_exponent=a, z_exponent=b, axis_phase_exponent=t) + assert cirq.is_parameterized(gt) + assert cirq.resolve_parameters(gt, {'t': c}) == g + + +def test_str_diagram(): + g = cirq.PhasedXZGate(x_exponent=0.5, + z_exponent=0.25, + axis_phase_exponent=0.125) + + assert str(g) == "PhXZ(a=0.125,x=0.5,z=0.25)" + + cirq.testing.assert_has_diagram( + cirq.Circuit(g.on(cirq.LineQubit(0))), """ +0: ───PhXZ(a=0.125,x=0.5,z=0.25)─── + """) diff --git a/cirq/protocols/json.py b/cirq/protocols/json.py index 51bdc9c4126..be7b236c57c 100644 --- a/cirq/protocols/json.py +++ b/cirq/protocols/json.py @@ -105,6 +105,7 @@ def _identity_operation_from_dict(qubits, **kwargs): 'PhaseGradientGate': cirq.PhaseGradientGate, 'PhasedISwapPowGate': cirq.PhasedISwapPowGate, 'PhasedXPowGate': cirq.PhasedXPowGate, + 'PhasedXZGate': cirq.PhasedXZGate, 'QuantumFourierTransformGate': cirq.QuantumFourierTransformGate, 'ResetChannel': cirq.ResetChannel, 'SingleQubitMatrixGate': cirq.SingleQubitMatrixGate, diff --git a/cirq/protocols/json_test_data/PhasedXZGate.json b/cirq/protocols/json_test_data/PhasedXZGate.json new file mode 100644 index 00000000000..ae2e3d64b94 --- /dev/null +++ b/cirq/protocols/json_test_data/PhasedXZGate.json @@ -0,0 +1,8 @@ +[ + { + "cirq_type": "PhasedXZGate", + "axis_phase_exponent": 0.75, + "x_exponent": 0.5, + "z_exponent": 0.25 + } +] \ No newline at end of file diff --git a/cirq/protocols/json_test_data/PhasedXZGate.repr b/cirq/protocols/json_test_data/PhasedXZGate.repr new file mode 100644 index 00000000000..bc742d2433d --- /dev/null +++ b/cirq/protocols/json_test_data/PhasedXZGate.repr @@ -0,0 +1,3 @@ +[ +cirq.PhasedXZGate(axis_phase_exponent=0.75, x_exponent=0.5, z_exponent=0.25), +] \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index 0321831dd1b..a12df052d2d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -88,6 +88,7 @@ Unitary effects that can be applied to one or more qubits. cirq.PhaseGradientGate cirq.PhasedISwapPowGate cirq.PhasedXPowGate + cirq.PhasedXZGate cirq.QuantumFourierTransformGate cirq.SingleQubitGate cirq.SingleQubitMatrixGate