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