1# Copyright 2021 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. 14import abc 15import collections 16import dataclasses 17import functools 18import math 19import re 20from typing import ( 21 Any, 22 Callable, 23 Dict, 24 Iterable, 25 List, 26 MutableMapping, 27 Optional, 28 Tuple, 29 TypeVar, 30 TYPE_CHECKING, 31 Generic, 32 Union, 33 cast, 34) 35 36import numpy as np 37import pandas as pd 38 39import cirq 40from cirq.experiments.xeb_fitting import ( 41 XEBPhasedFSimCharacterizationOptions, 42) 43from cirq_google.api import v2 44from cirq_google.engine import Calibration, CalibrationLayer, CalibrationResult, Engine, EngineJob 45from cirq_google.ops import SycamoreGate 46 47if TYPE_CHECKING: 48 import cirq_google 49 50 # Workaround for mypy custom dataclasses (python/mypy#5406) 51 from dataclasses import dataclass as json_serializable_dataclass 52else: 53 from cirq.protocols import json_serializable_dataclass 54 55 56_FLOQUET_PHASED_FSIM_HANDLER_NAME = 'floquet_phased_fsim_characterization' 57_XEB_PHASED_FSIM_HANDLER_NAME = 'xeb_phased_fsim_characterization' 58_DEFAULT_XEB_CYCLE_DEPTHS = (5, 25, 50, 100, 200, 300) 59 60T = TypeVar('T') 61 62RequestT = TypeVar('RequestT', bound='PhasedFSimCalibrationRequest') 63 64 65# Workaround for: https://github.com/python/mypy/issues/5858 66def lru_cache_typesafe(func: Callable[..., T]) -> T: 67 return functools.lru_cache(maxsize=None)(func) # type: ignore 68 69 70def _create_pairs_from_moment( 71 moment: cirq.Moment, 72) -> Tuple[Tuple[Tuple[cirq.Qid, cirq.Qid], ...], cirq.Gate]: 73 """Creates instantiation parameters from a Moment. 74 75 Given a moment, creates a tuple of pairs of qubits and the 76 gate for instantiation of a sub-class of PhasedFSimCalibrationRequest, 77 Sub-classes of PhasedFSimCalibrationRequest can call this function 78 to implement a from_moment function. 79 """ 80 gate = None 81 pairs: List[Tuple[cirq.Qid, cirq.Qid]] = [] 82 for op in moment: 83 if op.gate is None: 84 raise ValueError('All gates in request object must be two qubit gates: {op}') 85 if gate is None: 86 gate = op.gate 87 elif gate != op.gate: 88 raise ValueError('All gates in request object must be identical {gate}!={op.gate}') 89 if len(op.qubits) != 2: 90 raise ValueError('All gates in request object must be two qubit gates: {op}') 91 pairs.append((op.qubits[0], op.qubits[1])) 92 if gate is None: 93 raise ValueError('No gates found to create request {moment}') 94 return tuple(pairs), gate 95 96 97@json_serializable_dataclass(frozen=True) 98class PhasedFSimCharacterization: 99 """Holder for the unitary angles of the cirq.PhasedFSimGate. 100 101 This class stores five unitary parameters (θ, ζ, χ, γ and φ) that describe the 102 cirq.PhasedFSimGate which is the most general particle conserving two-qubit gate. The unitary 103 of the underlying gate is: 104 105 [[1, 0, 0, 0], 106 [0, exp(-i(γ + ζ)) cos(θ), -i exp(-i(γ - χ)) sin(θ), 0], 107 [0, -i exp(-i(γ + χ)) sin(θ), exp(-i(γ - ζ)) cos(θ), 0], 108 [0, 0, 0, exp(-i(2γ + φ))]] 109 110 The parameters θ, γ and φ are symmetric and parameters ζ and χ asymmetric under the qubits 111 exchange. 112 113 All the angles described by this class are optional and can be left unknown. This is relevant 114 for characterization routines that characterize only subset of the gate parameters. All the 115 angles are assumed to take a fixed numerical values which reflect the current state of the 116 characterized gate. 117 118 This class supports JSON serialization and deserialization. 119 120 Attributes: 121 theta: θ angle in radians or None when unknown. 122 zeta: ζ angle in radians or None when unknown. 123 chi: χ angle in radians or None when unknown. 124 gamma: γ angle in radians or None when unknown. 125 phi: φ angle in radians or None when unknown. 126 """ 127 128 theta: Optional[float] = None 129 zeta: Optional[float] = None 130 chi: Optional[float] = None 131 gamma: Optional[float] = None 132 phi: Optional[float] = None 133 134 def asdict(self) -> Dict[str, float]: 135 """Converts parameters to a dictionary that maps angle names to values.""" 136 return dataclasses.asdict(self) 137 138 def all_none(self) -> bool: 139 """Returns True if all the angles are None""" 140 return ( 141 self.theta is None 142 and self.zeta is None 143 and self.chi is None 144 and self.gamma is None 145 and self.phi is None 146 ) 147 148 def any_none(self) -> bool: 149 """Returns True if any the angle is None""" 150 return ( 151 self.theta is None 152 or self.zeta is None 153 or self.chi is None 154 or self.gamma is None 155 or self.phi is None 156 ) 157 158 def parameters_for_qubits_swapped(self) -> 'PhasedFSimCharacterization': 159 """Parameters for the gate with qubits swapped between each other. 160 161 The angles theta, gamma and phi are kept unchanged. The angles zeta and chi are negated for 162 the gate with swapped qubits. 163 164 Returns: 165 New instance with angles adjusted for swapped qubits. 166 """ 167 return PhasedFSimCharacterization( 168 theta=self.theta, 169 zeta=-self.zeta if self.zeta is not None else None, 170 chi=-self.chi if self.chi is not None else None, 171 gamma=self.gamma, 172 phi=self.phi, 173 ) 174 175 def merge_with(self, other: 'PhasedFSimCharacterization') -> 'PhasedFSimCharacterization': 176 """Substitutes missing parameter with values from other. 177 178 Args: 179 other: Parameters to use for None values. 180 181 Returns: 182 New instance of PhasedFSimCharacterization with values from this instance if they are 183 set or values from other when some parameter is None. 184 """ 185 return PhasedFSimCharacterization( 186 theta=other.theta if self.theta is None else self.theta, 187 zeta=other.zeta if self.zeta is None else self.zeta, 188 chi=other.chi if self.chi is None else self.chi, 189 gamma=other.gamma if self.gamma is None else self.gamma, 190 phi=other.phi if self.phi is None else self.phi, 191 ) 192 193 def override_by(self, other: 'PhasedFSimCharacterization') -> 'PhasedFSimCharacterization': 194 """Overrides other parameters that are not None. 195 196 Args: 197 other: Parameters to use for override. 198 199 Returns: 200 New instance of PhasedFSimCharacterization with values from other if set (values from 201 other that are not None). Otherwise the current values are used. 202 """ 203 return other.merge_with(self) 204 205 206SQRT_ISWAP_INV_PARAMETERS = PhasedFSimCharacterization( 207 theta=np.pi / 4, zeta=0.0, chi=0.0, gamma=0.0, phi=0.0 208) 209 210 211class PhasedFSimCalibrationOptions(abc.ABC, Generic[RequestT]): 212 """Base class for calibration-specific options passed together with the requests.""" 213 214 @abc.abstractmethod 215 def create_phased_fsim_request( 216 self, 217 pairs: Tuple[Tuple[cirq.Qid, cirq.Qid], ...], 218 gate: cirq.Gate, 219 ) -> RequestT: 220 """Create a PhasedFSimCalibrationRequest of the correct type for these options. 221 222 Args: 223 pairs: Set of qubit pairs to characterize. A single qubit can appear on at most one 224 pair in the set. 225 gate: Gate to characterize for each qubit pair from pairs. This must be a supported gate 226 which can be described cirq.PhasedFSim gate. This gate must be serialized by the 227 cirq_google.SerializableGateSet used 228 """ 229 230 231@dataclasses.dataclass 232class PhasedFSimCalibrationResult: 233 """The PhasedFSimGate characterization result. 234 235 Attributes: 236 parameters: Map from qubit pair to characterization result. For each pair of characterized 237 quibts a and b either only (a, b) or only (b, a) is present. 238 gate: Characterized gate for each qubit pair. This is copied from the matching 239 PhasedFSimCalibrationRequest and is included to preserve execution context. 240 options: The options used to gather this result. 241 project_id: Google's job project id. 242 program_id: Google's job program id. 243 job_id: Google's job job id. 244 """ 245 246 parameters: Dict[Tuple[cirq.Qid, cirq.Qid], PhasedFSimCharacterization] 247 gate: cirq.Gate 248 options: PhasedFSimCalibrationOptions 249 project_id: Optional[str] = None 250 program_id: Optional[str] = None 251 job_id: Optional[str] = None 252 _engine_job: Optional[EngineJob] = None 253 _calibration: Optional[Calibration] = None 254 255 def override(self, parameters: PhasedFSimCharacterization) -> 'PhasedFSimCalibrationResult': 256 """Creates the new results with certain parameters overridden for all characterizations. 257 258 This functionality can be used to zero-out the corrected angles and do the analysis on 259 remaining errors. 260 261 Args: 262 parameters: Parameters that will be used when overriding. The angles of that object 263 which are not None will be used to replace current parameters for every pair stored. 264 265 Returns: 266 New instance of PhasedFSimCalibrationResult with certain parameters overriden. 267 """ 268 return PhasedFSimCalibrationResult( 269 parameters={ 270 pair: pair_parameters.override_by(parameters) 271 for pair, pair_parameters in self.parameters.items() 272 }, 273 gate=self.gate, 274 options=self.options, 275 ) 276 277 def get_parameters(self, a: cirq.Qid, b: cirq.Qid) -> Optional['PhasedFSimCharacterization']: 278 """Returns parameters for a qubit pair (a, b) or None when unknown.""" 279 if (a, b) in self.parameters: 280 return self.parameters[(a, b)] 281 elif (b, a) in self.parameters: 282 return self.parameters[(b, a)].parameters_for_qubits_swapped() 283 else: 284 return None 285 286 @property 287 def engine_job(self) -> Optional[EngineJob]: 288 """The cirq_google.EngineJob associated with this calibration request. 289 290 Available only when project_id, program_id and job_id attributes are present. 291 """ 292 if self._engine_job is None and self.project_id and self.program_id and self.job_id: 293 engine = Engine(project_id=self.project_id) 294 self._engine_job = engine.get_program(self.program_id).get_job(self.job_id) 295 return self._engine_job 296 297 @property 298 def engine_calibration(self) -> Optional[Calibration]: 299 """The underlying device calibration that was used for this user-specific calibration. 300 301 This is a cached property that triggers a network call at the first use. 302 """ 303 if self._calibration is None and self.engine_job is not None: 304 self._calibration = self.engine_job.get_calibration() 305 return self._calibration 306 307 @classmethod 308 def _create_parameters_dict( 309 cls, 310 parameters: List[Tuple[cirq.Qid, cirq.Qid, PhasedFSimCharacterization]], 311 ) -> Dict[Tuple[cirq.Qid, cirq.Qid], PhasedFSimCharacterization]: 312 """Utility function to create parameters from JSON. 313 314 Can be used from child classes to instantiate classes in a _from_json_dict_ 315 method.""" 316 return {(q_a, q_b): params for q_a, q_b, params in parameters} 317 318 @classmethod 319 def _from_json_dict_( 320 cls, 321 **kwargs, 322 ) -> 'PhasedFSimCalibrationResult': 323 """Magic method for the JSON serialization protocol. 324 325 Converts serialized dictionary into a dict suitable for 326 class instantiation.""" 327 del kwargs['cirq_type'] 328 kwargs['parameters'] = cls._create_parameters_dict(kwargs['parameters']) 329 return cls(**kwargs) 330 331 def _json_dict_(self) -> Dict[str, Any]: 332 """Magic method for the JSON serialization protocol.""" 333 return { 334 'cirq_type': 'PhasedFSimCalibrationResult', 335 'gate': self.gate, 336 'parameters': [(q_a, q_b, params) for (q_a, q_b), params in self.parameters.items()], 337 'options': self.options, 338 'project_id': self.project_id, 339 'program_id': self.program_id, 340 'job_id': self.job_id, 341 } 342 343 344# TODO(#3388) Add documentation for Raises. 345# pylint: disable=missing-raises-doc 346def merge_matching_results( 347 results: Iterable[PhasedFSimCalibrationResult], 348) -> Optional[PhasedFSimCalibrationResult]: 349 """Merges a collection of results into a single result. 350 351 Args: 352 results: List of results to merge. They must be compatible with each other: all gate and 353 options fields must be equal and every characterized pair must be present only in one of 354 the characterizations. 355 356 Returns: 357 New PhasedFSimCalibrationResult that contains all the parameters from every result in 358 results or None when the results list is empty. 359 """ 360 all_parameters: Dict[Tuple[cirq.Qid, cirq.Qid], PhasedFSimCharacterization] = {} 361 common_gate = None 362 common_options = None 363 for result in results: 364 if common_gate is None: 365 common_gate = result.gate 366 elif common_gate != result.gate: 367 raise ValueError( 368 f'Only matching results can be merged, got gates {common_gate} and {result.gate}' 369 ) 370 371 if common_options is None: 372 common_options = result.options 373 elif common_options != result.options: 374 raise ValueError( 375 f'Only matching results can be merged, got options {common_options} and ' 376 f'{result.options}' 377 ) 378 379 if not all_parameters.keys().isdisjoint(result.parameters): 380 raise ValueError(f'Only results with disjoint parameters sets can be merged') 381 382 all_parameters.update(result.parameters) 383 384 if common_gate is None or common_options is None: 385 return None 386 387 return PhasedFSimCalibrationResult(all_parameters, common_gate, common_options) 388 389 390# pylint: enable=missing-raises-doc 391class PhasedFSimCalibrationError(Exception): 392 """Error that indicates the calibration failure.""" 393 394 395# We have to relax a mypy constraint, see https://github.com/python/mypy/issues/5374 396@dataclasses.dataclass(frozen=True) # type: ignore 397class PhasedFSimCalibrationRequest(abc.ABC): 398 """Description of the request to characterize PhasedFSimGate. 399 400 Attributes: 401 pairs: Set of qubit pairs to characterize. A single qubit can appear on at most one pair in 402 the set. 403 gate: Gate to characterize for each qubit pair from pairs. This must be a supported gate 404 which can be described cirq.PhasedFSim gate. This gate must be serialized by the 405 cirq_google.SerializableGateSet used 406 """ 407 408 pairs: Tuple[Tuple[cirq.Qid, cirq.Qid], ...] 409 gate: cirq.Gate # Any gate which can be described by cirq.PhasedFSim 410 options: PhasedFSimCalibrationOptions 411 412 # Workaround for: https://github.com/python/mypy/issues/1362 413 @property # type: ignore 414 @lru_cache_typesafe 415 def qubit_to_pair(self) -> MutableMapping[cirq.Qid, Tuple[cirq.Qid, cirq.Qid]]: 416 """Returns mapping from qubit to a qubit pair that it belongs to.""" 417 # Returning mutable mapping as a cached result because it's hard to get a frozen dictionary 418 # in Python... 419 return collections.ChainMap(*({q: pair for q in pair} for pair in self.pairs)) 420 421 @abc.abstractmethod 422 def to_calibration_layer(self) -> CalibrationLayer: 423 """Encodes this characterization request in a CalibrationLayer object.""" 424 425 @abc.abstractmethod 426 def parse_result( 427 self, result: CalibrationResult, job: Optional[EngineJob] = None 428 ) -> PhasedFSimCalibrationResult: 429 """Decodes the characterization result issued for this request.""" 430 431 432@json_serializable_dataclass(frozen=True) 433class XEBPhasedFSimCalibrationOptions(PhasedFSimCalibrationOptions): 434 """Options for configuring a PhasedFSim calibration using XEB. 435 436 XEB uses the fidelity of random circuits to characterize PhasedFSim gates. The parameters 437 of the gate are varied by a classical optimizer to maximize the observed fidelities. 438 439 Args: 440 n_library_circuits: The number of distinct, two-qubit random circuits to use in our 441 library of random circuits. This should be the same order of magnitude as 442 `n_combinations`. 443 n_combinations: We take each library circuit and randomly assign it to qubit pairs. 444 This parameter controls the number of random combinations of the two-qubit random 445 circuits we execute. Higher values increase the precision of estimates but linearly 446 increase experimental runtime. 447 cycle_depths: We run the random circuits at these cycle depths to fit an exponential 448 decay in the fidelity. 449 fatol: The absolute convergence tolerance for the objective function evaluation in 450 the Nelder-Mead optimization. This controls the runtime of the classical 451 characterization optimization loop. 452 xatol: The absolute convergence tolerance for the parameter estimates in 453 the Nelder-Mead optimization. This controls the runtime of the classical 454 characterization optimization loop. 455 fsim_options: An instance of `XEBPhasedFSimCharacterizationOptions` that controls aspects 456 of the PhasedFSim characterization like initial guesses and which angles to 457 characterize. 458 """ 459 460 n_library_circuits: int = 20 461 n_combinations: int = 10 462 cycle_depths: Tuple[int, ...] = _DEFAULT_XEB_CYCLE_DEPTHS 463 fatol: Optional[float] = 5e-3 464 xatol: Optional[float] = 5e-3 465 466 fsim_options: XEBPhasedFSimCharacterizationOptions = XEBPhasedFSimCharacterizationOptions() 467 468 def to_args(self) -> Dict[str, Any]: 469 """Convert this dataclass to an `args` dictionary suitable for sending to the Quantum 470 Engine calibration API.""" 471 args: Dict[str, Any] = { 472 'n_library_circuits': self.n_library_circuits, 473 'n_combinations': self.n_combinations, 474 'cycle_depths': '_'.join(f'{cd:d}' for cd in self.cycle_depths), 475 } 476 if self.fatol is not None: 477 args['fatol'] = self.fatol 478 if self.xatol is not None: 479 args['xatol'] = self.xatol 480 481 fsim_options = dataclasses.asdict(self.fsim_options) 482 fsim_options = {k: v for k, v in fsim_options.items() if v is not None} 483 args.update(fsim_options) 484 return args 485 486 def create_phased_fsim_request( 487 self, 488 pairs: Tuple[Tuple[cirq.Qid, cirq.Qid], ...], 489 gate: cirq.Gate, 490 ) -> 'XEBPhasedFSimCalibrationRequest': 491 return XEBPhasedFSimCalibrationRequest(pairs=pairs, gate=gate, options=self) 492 493 @classmethod 494 def _from_json_dict_(cls, **kwargs): 495 del kwargs['cirq_type'] 496 kwargs['cycle_depths'] = tuple(kwargs['cycle_depths']) 497 return cls(**kwargs) 498 499 500@json_serializable_dataclass(frozen=True) 501class LocalXEBPhasedFSimCalibrationOptions(XEBPhasedFSimCalibrationOptions): 502 """Options for configuring a PhasedFSim calibration using a local version of XEB. 503 504 XEB uses the fidelity of random circuits to characterize PhasedFSim gates. The parameters 505 of the gate are varied by a classical optimizer to maximize the observed fidelities. 506 507 These "Local" options (corresponding to `LocalXEBPhasedFSimCalibrationRequest`) instruct 508 `cirq_google.run_calibrations` to execute XEB analysis locally (not via the quantum 509 engine). As such, `run_calibrations` can work with any `cirq.Sampler`, not just 510 `QuantumEngineSampler`. 511 512 Args: 513 n_library_circuits: The number of distinct, two-qubit random circuits to use in our 514 library of random circuits. This should be the same order of magnitude as 515 `n_combinations`. 516 n_combinations: We take each library circuit and randomly assign it to qubit pairs. 517 This parameter controls the number of random combinations of the two-qubit random 518 circuits we execute. Higher values increase the precision of estimates but linearly 519 increase experimental runtime. 520 cycle_depths: We run the random circuits at these cycle depths to fit an exponential 521 decay in the fidelity. 522 fatol: The absolute convergence tolerance for the objective function evaluation in 523 the Nelder-Mead optimization. This controls the runtime of the classical 524 characterization optimization loop. 525 xatol: The absolute convergence tolerance for the parameter estimates in 526 the Nelder-Mead optimization. This controls the runtime of the classical 527 characterization optimization loop. 528 fsim_options: An instance of `XEBPhasedFSimCharacterizationOptions` that controls aspects 529 of the PhasedFSim characterization like initial guesses and which angles to 530 characterize. 531 n_processes: The number of multiprocessing processes to analyze the XEB characterization 532 data. By default, we use a value equal to the number of CPU cores. If `1` is specified, 533 multiprocessing is not used. 534 """ 535 536 n_processes: Optional[int] = None 537 538 def create_phased_fsim_request( 539 self, 540 pairs: Tuple[Tuple[cirq.Qid, cirq.Qid], ...], 541 gate: cirq.Gate, 542 ): 543 return LocalXEBPhasedFSimCalibrationRequest(pairs=pairs, gate=gate, options=self) 544 545 546@json_serializable_dataclass(frozen=True) 547class FloquetPhasedFSimCalibrationOptions(PhasedFSimCalibrationOptions): 548 """Options specific to Floquet PhasedFSimCalibration. 549 550 Some angles require another angle to be characterized first so result might have more angles 551 characterized than requested here. 552 553 Attributes: 554 characterize_theta: Whether to characterize θ angle. 555 characterize_zeta: Whether to characterize ζ angle. 556 characterize_chi: Whether to characterize χ angle. 557 characterize_gamma: Whether to characterize γ angle. 558 characterize_phi: Whether to characterize φ angle. 559 readout_error_tolerance: Threshold for pairwise-correlated readout errors above which the 560 calibration will report to fail. Just before each calibration all pairwise two-qubit 561 readout errors are checked and when any of the pairs reports an error above the 562 threshold, the calibration will fail. This value is a sanity check to determine if 563 calibration is reasonable and allows for quick termination if it is not. Set to 1.0 to 564 disable readout error checks and None to use default, device-specific thresholds. 565 """ 566 567 characterize_theta: bool 568 characterize_zeta: bool 569 characterize_chi: bool 570 characterize_gamma: bool 571 characterize_phi: bool 572 readout_error_tolerance: Optional[float] = None 573 574 def zeta_chi_gamma_correction_override(self) -> PhasedFSimCharacterization: 575 """Gives a PhasedFSimCharacterization that can be used to override characterization after 576 correcting for zeta, chi and gamma angles. 577 """ 578 return PhasedFSimCharacterization( 579 zeta=0.0 if self.characterize_zeta else None, 580 chi=0.0 if self.characterize_chi else None, 581 gamma=0.0 if self.characterize_gamma else None, 582 ) 583 584 def create_phased_fsim_request( 585 self, 586 pairs: Tuple[Tuple[cirq.Qid, cirq.Qid], ...], 587 gate: cirq.Gate, 588 ) -> 'FloquetPhasedFSimCalibrationRequest': 589 return FloquetPhasedFSimCalibrationRequest(pairs=pairs, gate=gate, options=self) 590 591 592"""Floquet PhasedFSimCalibrationOptions options with all angles characterization requests set to 593True.""" 594ALL_ANGLES_FLOQUET_PHASED_FSIM_CHARACTERIZATION = FloquetPhasedFSimCalibrationOptions( 595 characterize_theta=True, 596 characterize_zeta=True, 597 characterize_chi=True, 598 characterize_gamma=True, 599 characterize_phi=True, 600) 601 602"""XEB PhasedFSimCalibrationOptions options with all angles characterization requests set to 603True.""" 604ALL_ANGLES_XEB_PHASED_FSIM_CHARACTERIZATION = XEBPhasedFSimCalibrationOptions( 605 fsim_options=XEBPhasedFSimCharacterizationOptions( 606 characterize_theta=True, 607 characterize_zeta=True, 608 characterize_chi=True, 609 characterize_gamma=True, 610 characterize_phi=True, 611 ) 612) 613 614 615"""PhasedFSimCalibrationOptions with all but chi angle characterization requests set to True.""" 616WITHOUT_CHI_FLOQUET_PHASED_FSIM_CHARACTERIZATION = FloquetPhasedFSimCalibrationOptions( 617 characterize_theta=True, 618 characterize_zeta=True, 619 characterize_chi=False, 620 characterize_gamma=True, 621 characterize_phi=True, 622) 623 624 625"""PhasedFSimCalibrationOptions with theta, zeta and gamma angles characterization requests set to 626True. 627 628Those are the most efficient options that can be used to cancel out the errors by adding the 629appropriate single-qubit Z rotations to the circuit. The angles zeta, chi and gamma can be removed 630by those additions. The angle chi is disabled because it's not supported by Floquet characterization 631currently. The angle theta is set enabled because it is characterized together with zeta and adding 632it doesn't cost anything. 633""" 634THETA_ZETA_GAMMA_FLOQUET_PHASED_FSIM_CHARACTERIZATION = FloquetPhasedFSimCalibrationOptions( 635 characterize_theta=True, 636 characterize_zeta=True, 637 characterize_chi=False, 638 characterize_gamma=True, 639 characterize_phi=False, 640) 641 642 643@dataclasses.dataclass(frozen=True) 644class FloquetPhasedFSimCalibrationRequest(PhasedFSimCalibrationRequest): 645 """PhasedFSim characterization request specific to Floquet calibration. 646 647 Attributes: 648 options: Floquet-specific characterization options. 649 """ 650 651 options: FloquetPhasedFSimCalibrationOptions 652 653 @classmethod 654 def from_moment(cls, moment: cirq.Moment, options: FloquetPhasedFSimCalibrationOptions): 655 """Creates a FloquetPhasedFSimCalibrationRequest from a Moment. 656 657 Given a `Moment` object, this function extracts out the pairs of 658 qubits and the `Gate` used to create a `FloquetPhasedFSimCalibrationRequest` 659 object. The moment must contain only identical two-qubit FSimGates. 660 If dissimilar gates are passed in, a ValueError is raised. 661 """ 662 pairs, gate = _create_pairs_from_moment(moment) 663 return cls(pairs, gate, options) 664 665 def to_calibration_layer(self) -> CalibrationLayer: 666 circuit = cirq.Circuit(self.gate.on(*pair) for pair in self.pairs) 667 args: Dict[str, Any] = { 668 'est_theta': self.options.characterize_theta, 669 'est_zeta': self.options.characterize_zeta, 670 'est_chi': self.options.characterize_chi, 671 'est_gamma': self.options.characterize_gamma, 672 'est_phi': self.options.characterize_phi, 673 # Experimental option that should always be set to True. 674 'readout_corrections': True, 675 } 676 if self.options.readout_error_tolerance is not None: 677 # Maximum error of the diagonal elements of the two-qubit readout confusion matrix. 678 args['readout_error_tolerance'] = self.options.readout_error_tolerance 679 # Maximum error of the off-diagonal elements of the two-qubit readout confusion matrix. 680 args['correlated_readout_error_tolerance'] = _correlated_from_readout_tolerance( 681 self.options.readout_error_tolerance 682 ) 683 return CalibrationLayer( 684 calibration_type=_FLOQUET_PHASED_FSIM_HANDLER_NAME, 685 program=circuit, 686 args=args, 687 ) 688 689 def parse_result( 690 self, result: CalibrationResult, job: Optional[EngineJob] = None 691 ) -> PhasedFSimCalibrationResult: 692 if result.code != v2.calibration_pb2.SUCCESS: 693 raise PhasedFSimCalibrationError(result.error_message) 694 695 decoded: Dict[int, Dict[str, Any]] = collections.defaultdict(lambda: {}) 696 for keys, values in result.metrics['angles'].items(): 697 for key, value in zip(keys, values): 698 match = re.match(r'(\d+)_(.+)', str(key)) 699 if not match: 700 raise ValueError(f'Unknown metric name {key}') 701 index = int(match[1]) 702 name = match[2] 703 decoded[index][name] = value 704 705 parsed = {} 706 for data in decoded.values(): 707 a = v2.qubit_from_proto_id(data['qubit_a']) 708 b = v2.qubit_from_proto_id(data['qubit_b']) 709 parsed[(a, b)] = PhasedFSimCharacterization( 710 theta=data.get('theta_est', None), 711 zeta=data.get('zeta_est', None), 712 chi=data.get('chi_est', None), 713 gamma=data.get('gamma_est', None), 714 phi=data.get('phi_est', None), 715 ) 716 717 return PhasedFSimCalibrationResult( 718 parameters=parsed, 719 gate=self.gate, 720 options=self.options, 721 project_id=None if job is None else job.project_id, 722 program_id=None if job is None else job.program_id, 723 job_id=None if job is None else job.job_id, 724 ) 725 726 @classmethod 727 def _from_json_dict_( 728 cls, 729 gate: cirq.Gate, 730 pairs: List[Tuple[cirq.Qid, cirq.Qid]], 731 options: FloquetPhasedFSimCalibrationOptions, 732 **kwargs, 733 ) -> 'FloquetPhasedFSimCalibrationRequest': 734 """Magic method for the JSON serialization protocol. 735 736 Converts serialized dictionary into a dict suitable for 737 class instantiation.""" 738 instantiation_pairs = tuple((q_a, q_b) for q_a, q_b in pairs) 739 return cls(instantiation_pairs, gate, options) 740 741 def _json_dict_(self) -> Dict[str, Any]: 742 """Magic method for the JSON serialization protocol.""" 743 return { 744 'cirq_type': 'FloquetPhasedFSimCalibrationRequest', 745 'pairs': [(pair[0], pair[1]) for pair in self.pairs], 746 'gate': self.gate, 747 'options': self.options, 748 } 749 750 751def _correlated_from_readout_tolerance(readout_tolerance: float) -> float: 752 """Heuristic formula for the off-diagonal confusion matrix error thresholds. 753 754 This is chosen to return 0.3 for readout_tolerance = 0.4 and 1.0 for readout_tolerance = 1.0. 755 """ 756 return max(0.0, min(1.0, 7 / 6 * readout_tolerance - 1 / 6)) 757 758 759def _get_labeled_int(key: str, s: str): 760 ma = re.match(rf'{key}_(\d+)$', s) 761 if ma is None: 762 raise ValueError(f"Could not parse {key} value for {s}") 763 return int(ma.group(1)) 764 765 766def _parse_xeb_fidelities_df(metrics: 'cirq_google.Calibration', super_name: str) -> pd.DataFrame: 767 """Parse a fidelities DataFrame from Metric protos. 768 769 Args: 770 metrics: The metrics from a CalibrationResult 771 super_name: The metric name prefix. We will extract information for metrics named like 772 "{super_name}_depth_{depth}", so you can have multiple independent DataFrames in 773 one CalibrationResult. 774 """ 775 records: List[Dict[str, Union[int, float, Tuple[cirq.Qid, cirq.Qid]]]] = [] 776 for metric_name in metrics.keys(): 777 ma = re.match(fr'{super_name}_depth_(\d+)$', metric_name) 778 if ma is None: 779 continue 780 781 for (layer_str, pair_str, qa, qb), (value,) in metrics[metric_name].items(): 782 records.append( 783 { 784 'cycle_depth': int(ma.group(1)), 785 'layer_i': _get_labeled_int('layer', cast(str, layer_str)), 786 'pair_i': _get_labeled_int('pair', cast(str, pair_str)), 787 'fidelity': float(value), 788 'pair': (cast(cirq.GridQubit, qa), cast(cirq.GridQubit, qb)), 789 } 790 ) 791 return pd.DataFrame(records) 792 793 794def _parse_characterized_angles( 795 metrics: 'cirq_google.Calibration', 796 super_name: str, 797) -> Dict[Tuple[cirq.Qid, cirq.Qid], Dict[str, float]]: 798 """Parses characterized angles from Metric protos. 799 800 Args: 801 metrics: The metrics from a CalibrationResult 802 super_name: The metric name prefix. We extract angle names as "{super_name}_{angle_name}". 803 """ 804 805 records: Dict[Tuple[cirq.Qid, cirq.Qid], Dict[str, float]] = collections.defaultdict(dict) 806 for metric_name in metrics.keys(): 807 ma = re.match(fr'{super_name}_(\w+)$', metric_name) 808 if ma is None: 809 continue 810 811 angle_name = ma.group(1) 812 for (qa, qb), (value,) in metrics[metric_name].items(): 813 qa = cast(cirq.GridQubit, qa) 814 qb = cast(cirq.GridQubit, qb) 815 value = float(value) 816 records[qa, qb][angle_name] = value 817 return dict(records) 818 819 820@json_serializable_dataclass(frozen=True) 821class LocalXEBPhasedFSimCalibrationRequest(PhasedFSimCalibrationRequest): 822 """PhasedFSim characterization request for local cross entropy benchmarking (XEB) calibration. 823 824 A "Local" request (corresponding to `LocalXEBPhasedFSimCalibrationOptions`) instructs 825 `cirq_google.run_calibrations` to execute XEB analysis locally (not via the quantum 826 engine). As such, `run_calibrations` can work with any `cirq.Sampler`, not just 827 `QuantumEngineSampler`. 828 829 Attributes: 830 options: local-XEB-specific characterization options. 831 """ 832 833 options: LocalXEBPhasedFSimCalibrationOptions 834 835 def parse_result( 836 self, result: CalibrationResult, job: Optional[EngineJob] = None 837 ) -> PhasedFSimCalibrationResult: 838 raise NotImplementedError('Not applicable for local calibrations') 839 840 def to_calibration_layer(self) -> CalibrationLayer: 841 raise NotImplementedError('Not applicable for local calibrations') 842 843 @classmethod 844 def _from_json_dict_( 845 cls, 846 gate: cirq.Gate, 847 pairs: List[Tuple[cirq.Qid, cirq.Qid]], 848 options: LocalXEBPhasedFSimCalibrationOptions, 849 **kwargs, 850 ) -> 'LocalXEBPhasedFSimCalibrationRequest': 851 # List -> Tuple 852 instantiation_pairs = tuple((q_a, q_b) for q_a, q_b in pairs) 853 return cls(instantiation_pairs, gate, options) 854 855 856@json_serializable_dataclass(frozen=True) 857class XEBPhasedFSimCalibrationRequest(PhasedFSimCalibrationRequest): 858 """PhasedFSim characterization request for cross entropy benchmarking (XEB) calibration. 859 860 Attributes: 861 options: XEB-specific characterization options. 862 """ 863 864 options: XEBPhasedFSimCalibrationOptions 865 866 def to_calibration_layer(self) -> CalibrationLayer: 867 circuit = cirq.Circuit(self.gate.on(*pair) for pair in self.pairs) 868 return CalibrationLayer( 869 calibration_type=_XEB_PHASED_FSIM_HANDLER_NAME, 870 program=circuit, 871 args=self.options.to_args(), 872 ) 873 874 def parse_result( 875 self, result: CalibrationResult, job: Optional[EngineJob] = None 876 ) -> PhasedFSimCalibrationResult: 877 if result.code != v2.calibration_pb2.SUCCESS: 878 raise PhasedFSimCalibrationError(result.error_message) 879 880 # pylint: disable=unused-variable 881 initial_fids = _parse_xeb_fidelities_df(result.metrics, 'initial_fidelities') 882 final_fids = _parse_xeb_fidelities_df(result.metrics, 'final_fidelities') 883 # pylint: enable=unused-variable 884 885 final_params = { 886 pair: PhasedFSimCharacterization(**angles) 887 for pair, angles in _parse_characterized_angles( 888 result.metrics, 'characterized_angles' 889 ).items() 890 } 891 892 # TODO: Return initial_fids, final_fids somehow. 893 return PhasedFSimCalibrationResult( 894 parameters=final_params, 895 gate=self.gate, 896 options=self.options, 897 project_id=None if job is None else job.project_id, 898 program_id=None if job is None else job.program_id, 899 job_id=None if job is None else job.job_id, 900 ) 901 902 @classmethod 903 def _from_json_dict_( 904 cls, 905 gate: cirq.Gate, 906 pairs: List[Tuple[cirq.Qid, cirq.Qid]], 907 options: XEBPhasedFSimCalibrationOptions, 908 **kwargs, 909 ) -> 'XEBPhasedFSimCalibrationRequest': 910 # List -> Tuple 911 instantiation_pairs = tuple((q_a, q_b) for q_a, q_b in pairs) 912 return cls(instantiation_pairs, gate, options) 913 914 915class IncompatibleMomentError(Exception): 916 """Error that occurs when a moment is not supported by a calibration routine.""" 917 918 919@dataclasses.dataclass(frozen=True) 920class PhaseCalibratedFSimGate: 921 """Association of a user gate with gate to calibrate. 922 923 This association stores information regarding rotation of the calibrated FSim gate by 924 phase_exponent p: 925 926 (Z^-p ⊗ Z^p) FSim (Z^p ⊗ Z^-p). 927 928 The rotation should be reflected back during the compilation after the gate is calibrated and 929 is equivalent to the shift of -2πp in the χ angle of PhasedFSimGate. 930 931 Attributes: 932 engine_gate: Gate that should be used for calibration purposes. 933 phase_exponent: Phase rotation exponent p. 934 """ 935 936 engine_gate: cirq.FSimGate 937 phase_exponent: float 938 939 def as_characterized_phased_fsim_gate( 940 self, parameters: PhasedFSimCharacterization 941 ) -> cirq.PhasedFSimGate: 942 """Creates a PhasedFSimGate which represents the characterized engine_gate but includes 943 deviations in unitary parameters. 944 945 Args: 946 parameters: The results of characterization of the engine gate. 947 948 Returns: 949 Instance of PhasedFSimGate that executes a gate according to the characterized 950 parameters of the engine_gate. 951 """ 952 return cirq.PhasedFSimGate( 953 theta=parameters.theta, 954 zeta=parameters.zeta, 955 chi=parameters.chi - 2 * np.pi * self.phase_exponent, 956 gamma=parameters.gamma, 957 phi=parameters.phi, 958 ) 959 960 def with_zeta_chi_gamma_compensated( 961 self, 962 qubits: Tuple[cirq.Qid, cirq.Qid], 963 parameters: PhasedFSimCharacterization, 964 *, 965 engine_gate: Optional[cirq.Gate] = None, 966 ) -> Tuple[Tuple[cirq.Operation, ...], ...]: 967 """Creates a composite operation that compensates for zeta, chi and gamma angles of the 968 characterization. 969 970 Args: 971 qubits: Qubits that the gate should act on. 972 parameters: The results of characterization of the engine gate. 973 engine_gate: 2-qubit gate that represents the engine gate. When None, the internal 974 engine_gate of this instance is used. This argument is useful for testing 975 purposes. 976 977 Returns: 978 Tuple of tuple of operations that describe the compensated gate. The first index 979 iterates over moments of the composed operation. 980 981 Raises: 982 ValueError: If the engine gate is not a 2-qubit gate. 983 """ 984 assert parameters.zeta is not None, "Zeta value must not be None" 985 zeta = parameters.zeta 986 987 assert parameters.gamma is not None, "Gamma value must not be None" 988 gamma = parameters.gamma 989 990 assert parameters.chi is not None, "Chi value must not be None" 991 chi = parameters.chi + 2 * np.pi * self.phase_exponent 992 993 if engine_gate is None: 994 engine_gate = self.engine_gate 995 else: 996 if cirq.num_qubits(engine_gate) != 2: 997 raise ValueError('Engine gate must be a two-qubit gate') # coverage: ignore 998 999 a, b = qubits 1000 1001 alpha = 0.5 * (zeta + chi) 1002 beta = 0.5 * (zeta - chi) 1003 1004 return ( 1005 (cirq.rz(0.5 * gamma - alpha).on(a), cirq.rz(0.5 * gamma + alpha).on(b)), 1006 (engine_gate.on(a, b),), 1007 (cirq.rz(0.5 * gamma - beta).on(a), cirq.rz(0.5 * gamma + beta).on(b)), 1008 ) 1009 1010 def _unitary_(self) -> np.array: 1011 """Implements Cirq's `unitary` protocol for this object.""" 1012 p = np.exp(-np.pi * 1j * self.phase_exponent) 1013 return ( 1014 np.diag([1, p, 1 / p, 1]) @ cirq.unitary(self.engine_gate) @ np.diag([1, 1 / p, p, 1]) 1015 ) 1016 1017 1018def try_convert_sqrt_iswap_to_fsim(gate: cirq.Gate) -> Optional[PhaseCalibratedFSimGate]: 1019 """Converts an equivalent gate to FSimGate(theta=π/4, phi=0) if possible. 1020 1021 Args: 1022 gate: Gate to verify. 1023 1024 Returns: 1025 FSimGateCalibration with engine_gate FSimGate(theta=π/4, phi=0) if the provided gate is 1026 either FSimGate, ISWapPowGate, PhasedFSimGate or PhasedISwapPowGate that is equivalent to 1027 FSimGate(theta=±π/4, phi=0). None otherwise. 1028 """ 1029 result = try_convert_gate_to_fsim(gate) 1030 if result is None: 1031 return None 1032 engine_gate = result.engine_gate 1033 if math.isclose(engine_gate.theta, np.pi / 4) and math.isclose(engine_gate.phi, 0.0): 1034 return result 1035 return None 1036 1037 1038def try_convert_gate_to_fsim(gate: cirq.Gate) -> Optional[PhaseCalibratedFSimGate]: 1039 """Converts a gate to equivalent PhaseCalibratedFSimGate if possible. 1040 1041 Args: 1042 gate: Gate to convert. 1043 1044 Returns: 1045 If provided gate is equivalent to some PhaseCalibratedFSimGate, returns that gate. 1046 Otherwise returns None. 1047 """ 1048 if cirq.is_parameterized(gate): 1049 return None 1050 1051 phi = 0.0 1052 theta = 0.0 1053 phase_exponent = 0.0 1054 if isinstance(gate, SycamoreGate): 1055 phi = np.pi / 6 1056 theta = np.pi / 2 1057 elif isinstance(gate, cirq.FSimGate): 1058 theta = gate.theta 1059 phi = gate.phi 1060 elif isinstance(gate, cirq.ISwapPowGate): 1061 if not np.isclose(np.exp(np.pi * 1j * gate.global_shift * gate.exponent), 1.0): 1062 return None 1063 theta = -gate.exponent * np.pi / 2 1064 elif isinstance(gate, cirq.PhasedFSimGate): 1065 if not np.isclose(gate.zeta, 0.0) or not np.isclose(gate.gamma, 0.0): 1066 return None 1067 theta = gate.theta 1068 phi = gate.phi 1069 phase_exponent = -gate.chi / (2 * np.pi) 1070 elif isinstance(gate, cirq.PhasedISwapPowGate): 1071 theta = -gate.exponent * np.pi / 2 1072 phase_exponent = -gate.phase_exponent 1073 elif isinstance(gate, cirq.ops.CZPowGate): 1074 if not np.isclose(np.exp(np.pi * 1j * gate.global_shift * gate.exponent), 1.0): 1075 return None 1076 phi = -np.pi * gate.exponent 1077 else: 1078 return None 1079 1080 phi = phi % (2 * np.pi) 1081 theta = theta % (2 * np.pi) 1082 if theta >= np.pi: 1083 theta = 2 * np.pi - theta 1084 phase_exponent = phase_exponent + 0.5 1085 phase_exponent %= 1 1086 return PhaseCalibratedFSimGate(cirq.FSimGate(theta=theta, phi=phi), phase_exponent) 1087 1088 1089def try_convert_syc_or_sqrt_iswap_to_fsim( 1090 gate: cirq.Gate, 1091) -> Optional[PhaseCalibratedFSimGate]: 1092 """Converts a gate to equivalent PhaseCalibratedFSimGate if possible. 1093 1094 Args: 1095 gate: Gate to convert. 1096 1097 Returns: 1098 Equivalent PhaseCalibratedFSimGate if its `engine_gate` is Sycamore or inverse sqrt(iSWAP) 1099 gate. Otherwise returns None. 1100 """ 1101 result = try_convert_gate_to_fsim(gate) 1102 if result is None: 1103 return None 1104 engine_gate = result.engine_gate 1105 if math.isclose(engine_gate.theta, np.pi / 2) and math.isclose(engine_gate.phi, np.pi / 6): 1106 # Sycamore gate. 1107 return result 1108 if math.isclose(engine_gate.theta, np.pi / 4) and math.isclose(engine_gate.phi, 0.0): 1109 # Inverse sqrt(iSWAP) gate. 1110 return result 1111 return None 1112