1# Copyright 2019 The Cirq Developers
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Defines the fermionic simulation gate family.
15
16This is the family of two-qubit gates that preserve excitations (number of ON
17qubits), ignoring single-qubit gates and global phase. For example, when using
18the second quantized representation of electrons to simulate chemistry, this is
19a natural gateset because each ON qubit corresponds to an electron and in the
20context of chemistry the electron count is conserved over time. This property
21applies more generally to fermions, thus the name of the gate.
22"""
23
24import cmath
25import math
26from typing import AbstractSet, Any, Dict, Optional, Tuple, Union
27
28import numpy as np
29import sympy
30
31import cirq
32from cirq import protocols, value
33from cirq._compat import proper_repr
34from cirq.ops import gate_features, raw_types
35
36
37def _canonicalize(value: Union[float, sympy.Basic]) -> Union[float, sympy.Basic]:
38    """Assumes value is 2π-periodic and shifts it into [-π, π]."""
39    if protocols.is_parameterized(value):
40        return value
41    period = 2 * np.pi
42    return value - period * np.round(value / period)
43
44
45def _zero_mod_pi(param: Union[float, sympy.Basic]) -> bool:
46    """Returns True iff param, assumed to be in [-pi, pi], is 0 (mod pi)."""
47    return param in (-np.pi, 0.0, np.pi, -sympy.pi, sympy.pi)
48
49
50def _half_pi_mod_pi(param: Union[float, sympy.Basic]) -> bool:
51    """Returns True iff param, assumed to be in [-pi, pi], is pi/2 (mod pi)."""
52    return param in (-np.pi / 2, np.pi / 2, -sympy.pi / 2, sympy.pi / 2)
53
54
55@value.value_equality(approximate=True)
56class FSimGate(gate_features.InterchangeableQubitsGate, raw_types.Gate):
57    """Fermionic simulation gate family.
58
59    Contains all two qubit interactions that preserve excitations, up to
60    single-qubit rotations and global phase.
61
62    The unitary matrix of this gate is:
63
64        [[1, 0, 0, 0],
65         [0, a, b, 0],
66         [0, b, a, 0],
67         [0, 0, 0, c]]
68
69    where:
70
71        a = cos(theta)
72        b = -i·sin(theta)
73        c = exp(-i·phi)
74
75    Note the difference in sign conventions between FSimGate and the
76    ISWAP and CZPowGate:
77
78        FSimGate(θ, φ) = ISWAP**(-2θ/π) CZPowGate(exponent=-φ/π)
79    """
80
81    def __init__(self, theta: float, phi: float) -> None:
82        """Inits FSimGate.
83
84        Args:
85            theta: Swap angle on the ``|01⟩`` ``|10⟩`` subspace, in radians.
86                Determined by the strength and duration of the XX+YY
87                interaction. Note: uses opposite sign convention to the
88                iSWAP gate. Maximum strength (full iswap) is at pi/2.
89            phi: Controlled phase angle, in radians. Determines how much the
90                ``|11⟩`` state is phased. Note: uses opposite sign convention to
91                the CZPowGate. Maximum strength (full cz) is at pi.
92        """
93        self.theta = _canonicalize(theta)
94        self.phi = _canonicalize(phi)
95
96    def _num_qubits_(self) -> int:
97        return 2
98
99    def _value_equality_values_(self) -> Any:
100        return self.theta, self.phi
101
102    def _is_parameterized_(self) -> bool:
103        return cirq.is_parameterized(self.theta) or cirq.is_parameterized(self.phi)
104
105    def _parameter_names_(self) -> AbstractSet[str]:
106        return cirq.parameter_names(self.theta) | cirq.parameter_names(self.phi)
107
108    def _has_unitary_(self):
109        return not self._is_parameterized_()
110
111    def _unitary_(self) -> Optional[np.ndarray]:
112        if self._is_parameterized_():
113            return None
114        a = math.cos(self.theta)
115        b = -1j * math.sin(self.theta)
116        c = cmath.exp(-1j * self.phi)
117        return np.array(
118            [
119                [1, 0, 0, 0],
120                [0, a, b, 0],
121                [0, b, a, 0],
122                [0, 0, 0, c],
123            ]
124        )
125
126    def _pauli_expansion_(self) -> value.LinearDict[str]:
127        if protocols.is_parameterized(self):
128            return NotImplemented
129        a = math.cos(self.theta)
130        b = -1j * math.sin(self.theta)
131        c = cmath.exp(-1j * self.phi)
132        return value.LinearDict(
133            {
134                'II': (1 + c) / 4 + a / 2,
135                'IZ': (1 - c) / 4,
136                'ZI': (1 - c) / 4,
137                'ZZ': (1 + c) / 4 - a / 2,
138                'XX': b / 2,
139                'YY': b / 2,
140            }
141        )
142
143    def _resolve_parameters_(
144        self, resolver: 'cirq.ParamResolver', recursive: bool
145    ) -> 'cirq.FSimGate':
146        return FSimGate(
147            protocols.resolve_parameters(self.theta, resolver, recursive),
148            protocols.resolve_parameters(self.phi, resolver, recursive),
149        )
150
151    def _apply_unitary_(self, args: 'cirq.ApplyUnitaryArgs') -> Optional[np.ndarray]:
152        if cirq.is_parameterized(self):
153            return None
154        if self.theta != 0:
155            inner_matrix = protocols.unitary(cirq.rx(2 * self.theta))
156            oi = args.subspace_index(0b01)
157            io = args.subspace_index(0b10)
158            out = cirq.apply_matrix_to_slices(
159                args.target_tensor, inner_matrix, slices=[oi, io], out=args.available_buffer
160            )
161        else:
162            out = args.target_tensor
163        if self.phi != 0:
164            ii = args.subspace_index(0b11)
165            out[ii] *= cmath.exp(-1j * self.phi)
166        return out
167
168    def _decompose_(self, qubits) -> 'cirq.OP_TREE':
169        a, b = qubits
170        xx = cirq.XXPowGate(exponent=self.theta / np.pi, global_shift=-0.5)
171        yy = cirq.YYPowGate(exponent=self.theta / np.pi, global_shift=-0.5)
172        yield xx(a, b)
173        yield yy(a, b)
174        yield cirq.CZ(a, b) ** (-self.phi / np.pi)
175
176    def _circuit_diagram_info_(self, args: 'cirq.CircuitDiagramInfoArgs') -> Tuple[str, ...]:
177        t = args.format_radians(self.theta)
178        p = args.format_radians(self.phi)
179        return f'FSim({t}, {p})', f'FSim({t}, {p})'
180
181    def __pow__(self, power) -> 'FSimGate':
182        return FSimGate(cirq.mul(self.theta, power), cirq.mul(self.phi, power))
183
184    def __repr__(self) -> str:
185        t = proper_repr(self.theta)
186        p = proper_repr(self.phi)
187        return f'cirq.FSimGate(theta={t}, phi={p})'
188
189    def _json_dict_(self) -> Dict[str, Any]:
190        return protocols.obj_to_dict_helper(self, ['theta', 'phi'])
191
192
193@value.value_equality(approximate=True)
194class PhasedFSimGate(gate_features.InterchangeableQubitsGate, raw_types.Gate):
195    """General excitation-preserving two-qubit gate.
196
197    The unitary matrix of PhasedFSimGate(θ, ζ, χ, γ, φ) is:
198
199        [[1,                       0,                       0,            0],
200         [0,    exp(-iγ - iζ) cos(θ), -i exp(-iγ + iχ) sin(θ),            0],
201         [0, -i exp(-iγ - iχ) sin(θ),    exp(-iγ + iζ) cos(θ),            0],
202         [0,                       0,                       0, exp(-2iγ-iφ)]].
203
204    This parametrization follows eq (18) in https://arxiv.org/abs/2010.07965.
205    See also eq (43) in https://arxiv.org/abs/1910.11333 for an older variant
206    which uses the same θ and φ parameters, but its three phase angles have
207    different names and opposite sign. Specifically, ∆+ angle corresponds to
208    -γ, ∆- corresponds to -ζ and ∆-,off corresponds to -χ.
209
210    Another useful parametrization of PhasedFSimGate is based on the fact that
211    the gate is equivalent up to global phase to the following circuit:
212
213        0: ───Rz(α0)───FSim(θ, φ)───Rz(β0)───
214215        1: ───Rz(α1)───FSim(θ, φ)───Rz(β1)───
216
217    where α0 and α1 are Rz angles to be applied before the core FSimGate,
218    β0 and β1 are Rz angles to be applied after FSimGate and θ and φ specify
219    the core FSimGate. Use the static factory function from_fsim_rz to
220    instantiate the gate using this parametrization.
221
222    Note that the θ and φ parameters in the two parametrizations are the same.
223
224    The matrix above is block diagonal where the middle block may be any
225    element of U(2) and the bottom right block may be any element of U(1).
226    Consequently, five real parameters are required to specify an instance
227    of PhasedFSimGate. Therefore, the second parametrization is not injective.
228    Indeed, for any angle δ
229
230        cirq.PhasedFSimGate.from_fsim_rz(θ, φ, (α0, α1), (β0, β1))
231
232    and
233
234        cirq.PhasedFSimGate.from_fsim_rz(θ, φ,
235                                         (α0 + δ, α1 + δ),
236                                         (β0 - δ, β1 - δ))
237
238    specify the same gate and therefore the two instances will compare as
239    equal up to numerical error. Another consequence of the non-injective
240    character of the second parametrization is the fact that the properties
241    rz_angles_before and rz_angles_after may return different Rz angles
242    than the ones used in the call to from_fsim_rz.
243
244    This gate is generally not symmetric under exchange of qubits. It becomes
245    symmetric if both of the following conditions are satisfied:
246     * ζ = kπ or θ = π/2 + lπ for k and l integers,
247     * χ = kπ or θ = lπ for k and l integers.
248    """
249
250    def __init__(
251        self,
252        theta: Union[float, sympy.Basic],
253        zeta: Union[float, sympy.Basic] = 0.0,
254        chi: Union[float, sympy.Basic] = 0.0,
255        gamma: Union[float, sympy.Basic] = 0.0,
256        phi: Union[float, sympy.Basic] = 0.0,
257    ) -> None:
258        """Inits PhasedFSimGate.
259
260        Args:
261            theta: Swap angle on the ``|01⟩`` ``|10⟩`` subspace, in radians.
262                See class docstring above for details.
263            zeta: One of the phase angles, in radians. See class
264                docstring above for details.
265            chi: One of the phase angles, in radians.
266                See class docstring above for details.
267            gamma: One of the phase angles, in radians. See class
268                docstring above for details.
269            phi: Controlled phase angle, in radians. See class docstring
270                above for details.
271        """
272        self.theta = _canonicalize(theta)
273        self.zeta = _canonicalize(zeta)
274        self.chi = _canonicalize(chi)
275        self.gamma = _canonicalize(gamma)
276        self.phi = _canonicalize(phi)
277
278    @staticmethod
279    def from_fsim_rz(
280        theta: Union[float, sympy.Basic],
281        phi: Union[float, sympy.Basic],
282        rz_angles_before: Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]],
283        rz_angles_after: Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]],
284    ) -> 'PhasedFSimGate':
285        """Creates PhasedFSimGate using an alternate parametrization.
286
287        Args:
288            theta: Swap angle on the ``|01⟩`` ``|10⟩`` subspace, in radians.
289                See class docstring above for details.
290            phi: Controlled phase angle, in radians. See class docstring
291                above for details.
292            rz_angles_before: 2-tuple of phase angles to apply to each qubit
293                before the core FSimGate. See class docstring for details.
294            rz_angles_after: 2-tuple of phase angles to apply to each qubit
295                after the core FSimGate. See class docstring for details.
296        """
297        b0, b1 = rz_angles_before
298        a0, a1 = rz_angles_after
299        gamma = (-b0 - b1 - a0 - a1) / 2.0
300        zeta = (b0 - b1 + a0 - a1) / 2.0
301        chi = (b0 - b1 - a0 + a1) / 2.0
302        return PhasedFSimGate(theta, zeta, chi, gamma, phi)
303
304    @property
305    def rz_angles_before(self) -> Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]]:
306        """Returns 2-tuple of phase angles applied to qubits before FSimGate."""
307        b0 = (-self.gamma + self.zeta + self.chi) / 2.0
308        b1 = (-self.gamma - self.zeta - self.chi) / 2.0
309        return b0, b1
310
311    @property
312    def rz_angles_after(self) -> Tuple[Union[float, sympy.Basic], Union[float, sympy.Basic]]:
313        """Returns 2-tuple of phase angles applied to qubits after FSimGate."""
314        a0 = (-self.gamma + self.zeta - self.chi) / 2.0
315        a1 = (-self.gamma - self.zeta + self.chi) / 2.0
316        return a0, a1
317
318    def _zeta_insensitive(self) -> bool:
319        return _half_pi_mod_pi(self.theta)
320
321    def _chi_insensitive(self) -> bool:
322        return _zero_mod_pi(self.theta)
323
324    def qubit_index_to_equivalence_group_key(self, index: int) -> int:
325        """Returns a key that differs between non-interchangeable qubits."""
326        if (_zero_mod_pi(self.zeta) or self._zeta_insensitive()) and (
327            _zero_mod_pi(self.chi) or self._chi_insensitive()
328        ):
329            return 0
330        return index
331
332    def _value_equality_values_(self) -> Any:
333        if self._zeta_insensitive():
334            return (self.theta, 0.0, self.chi, self.gamma, self.phi)
335        if self._chi_insensitive():
336            return (self.theta, self.zeta, 0.0, self.gamma, self.phi)
337        return (self.theta, self.zeta, self.chi, self.gamma, self.phi)
338
339    def _is_parameterized_(self) -> bool:
340        return (
341            cirq.is_parameterized(self.theta)
342            or cirq.is_parameterized(self.zeta)
343            or cirq.is_parameterized(self.chi)
344            or cirq.is_parameterized(self.gamma)
345            or cirq.is_parameterized(self.phi)
346        )
347
348    def _has_unitary_(self):
349        return not self._is_parameterized_()
350
351    def _unitary_(self) -> Optional[np.ndarray]:
352        if self._is_parameterized_():
353            return None
354        a = math.cos(self.theta)
355        b = -1j * math.sin(self.theta)
356        c = cmath.exp(-1j * self.phi)
357        f1 = cmath.exp(-1j * self.gamma - 1j * self.zeta)
358        f2 = cmath.exp(-1j * self.gamma + 1j * self.chi)
359        f3 = cmath.exp(-1j * self.gamma - 1j * self.chi)
360        f4 = cmath.exp(-1j * self.gamma + 1j * self.zeta)
361        f5 = cmath.exp(-2j * self.gamma)
362        return np.array(
363            [
364                [1, 0, 0, 0],
365                [0, f1 * a, f2 * b, 0],
366                [0, f3 * b, f4 * a, 0],
367                [0, 0, 0, f5 * c],
368            ]
369        )
370
371    def _resolve_parameters_(
372        self, resolver: 'cirq.ParamResolver', recursive: bool
373    ) -> 'cirq.PhasedFSimGate':
374        return PhasedFSimGate(
375            protocols.resolve_parameters(self.theta, resolver, recursive),
376            protocols.resolve_parameters(self.zeta, resolver, recursive),
377            protocols.resolve_parameters(self.chi, resolver, recursive),
378            protocols.resolve_parameters(self.gamma, resolver, recursive),
379            protocols.resolve_parameters(self.phi, resolver, recursive),
380        )
381
382    def _apply_unitary_(self, args: 'cirq.ApplyUnitaryArgs') -> Optional[np.ndarray]:
383        if cirq.is_parameterized(self):
384            return None
385        oi = args.subspace_index(0b01)
386        io = args.subspace_index(0b10)
387        ii = args.subspace_index(0b11)
388        if self.theta != 0 or self.zeta != 0 or self.chi != 0:
389            rx = protocols.unitary(cirq.rx(2 * self.theta))
390            rz1 = protocols.unitary(cirq.rz(-self.zeta + self.chi))
391            rz2 = protocols.unitary(cirq.rz(-self.zeta - self.chi))
392            inner_matrix = rz1 @ rx @ rz2
393            out = cirq.apply_matrix_to_slices(
394                args.target_tensor, inner_matrix, slices=[oi, io], out=args.available_buffer
395            )
396        else:
397            out = args.target_tensor
398        if self.phi != 0:
399            out[ii] *= cmath.exp(-1j * self.phi)
400        if self.gamma != 0:
401            f = cmath.exp(-1j * self.gamma)
402            out[oi] *= f
403            out[io] *= f
404            out[ii] *= f * f
405        return out
406
407    def _decompose_(self, qubits) -> 'cirq.OP_TREE':
408        """Decomposes self into Z rotations and FSimGate.
409
410        Note that Z rotations returned by this method have unusual global phase
411        in that one of their eigenvalues is 1. This ensures the decomposition
412        agrees with the matrix specified in class docstring. In particular, it
413        makes the top left element of the matrix equal to 1.
414        """
415
416        def to_exponent(angle_rads: Union[float, sympy.Basic]) -> Union[float, sympy.Basic]:
417            """Divides angle_rads by symbolic or numerical pi."""
418            pi = sympy.pi if protocols.is_parameterized(angle_rads) else np.pi
419            return angle_rads / pi
420
421        q0, q1 = qubits
422        before = self.rz_angles_before
423        after = self.rz_angles_after
424        yield cirq.Z(q0) ** to_exponent(before[0])
425        yield cirq.Z(q1) ** to_exponent(before[1])
426        yield FSimGate(self.theta, self.phi).on(q0, q1)
427        yield cirq.Z(q0) ** to_exponent(after[0])
428        yield cirq.Z(q1) ** to_exponent(after[1])
429
430    def _circuit_diagram_info_(self, args: 'cirq.CircuitDiagramInfoArgs') -> Tuple[str, ...]:
431        theta = args.format_radians(self.theta)
432        zeta = args.format_radians(self.zeta)
433        chi = args.format_radians(self.chi)
434        gamma = args.format_radians(self.gamma)
435        phi = args.format_radians(self.phi)
436        return (
437            f'PhFSim({theta}, {zeta}, {chi}, {gamma}, {phi})',
438            f'PhFSim({theta}, {zeta}, {chi}, {gamma}, {phi})',
439        )
440
441    def __repr__(self) -> str:
442        theta = proper_repr(self.theta)
443        zeta = proper_repr(self.zeta)
444        chi = proper_repr(self.chi)
445        gamma = proper_repr(self.gamma)
446        phi = proper_repr(self.phi)
447        return (
448            f'cirq.PhasedFSimGate(theta={theta}, zeta={zeta}, chi={chi}, '
449            f'gamma={gamma}, phi={phi})'
450        )
451
452    def _json_dict_(self) -> Dict[str, Any]:
453        return protocols.obj_to_dict_helper(self, ['theta', 'zeta', 'chi', 'gamma', 'phi'])
454
455    def _num_qubits_(self) -> int:
456        return 2
457