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)─── 214 │ 215 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