Skip to content

Commit b8af1d3

Browse files
Support creating XEB calibration options for arbitary FSim like gates (quantumlib#6657)
This PR supports creating FSim calibration options for arbitary FSim like gates. It also split the XEB function in two in prerpartion for implmenting XEB calibration for arbitary gates. part of breaking quantumlib#6568 into smaller PRs.
1 parent cea8e1a commit b8af1d3

File tree

3 files changed

+156
-14
lines changed

3 files changed

+156
-14
lines changed

cirq/experiments/two_qubit_xeb.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def plot_histogram(
347347
return ax
348348

349349

350-
def parallel_two_qubit_xeb(
350+
def parallel_xeb_workflow(
351351
sampler: 'cirq.Sampler',
352352
qubits: Optional[Sequence['cirq.GridQubit']] = None,
353353
entangling_gate: 'cirq.Gate' = ops.CZ,
@@ -358,8 +358,8 @@ def parallel_two_qubit_xeb(
358358
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
359359
ax: Optional[plt.Axes] = None,
360360
**plot_kwargs,
361-
) -> TwoQubitXEBResult:
362-
"""A convenience method that runs the full XEB workflow.
361+
) -> Tuple[pd.DataFrame, Sequence['cirq.Circuit'], pd.DataFrame]:
362+
"""A utility method that runs the full XEB workflow.
363363
364364
Args:
365365
sampler: The quantum engine or simulator to run the circuits.
@@ -375,7 +375,12 @@ def parallel_two_qubit_xeb(
375375
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
376376
377377
Returns:
378-
A TwoQubitXEBResult object representing the results of the experiment.
378+
- A DataFrame with columns 'cycle_depth' and 'fidelity'.
379+
- The circuits used to perform XEB.
380+
- A pandas dataframe with index given by ['circuit_i', 'cycle_depth'].
381+
Columns always include "sampled_probs". If `combinations_by_layer` is
382+
not `None` and you are doing parallel XEB, additional metadata columns
383+
will be attached to the returned DataFrame.
379384
380385
Raises:
381386
ValueError: If qubits are not specified and the sampler has no device.
@@ -420,6 +425,52 @@ def parallel_two_qubit_xeb(
420425
sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths
421426
)
422427

428+
return fids, circuit_library, sampled_df
429+
430+
431+
def parallel_two_qubit_xeb(
432+
sampler: 'cirq.Sampler',
433+
qubits: Optional[Sequence['cirq.GridQubit']] = None,
434+
entangling_gate: 'cirq.Gate' = ops.CZ,
435+
n_repetitions: int = 10**4,
436+
n_combinations: int = 10,
437+
n_circuits: int = 20,
438+
cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)),
439+
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
440+
ax: Optional[plt.Axes] = None,
441+
**plot_kwargs,
442+
) -> TwoQubitXEBResult:
443+
"""A convenience method that runs the full XEB workflow.
444+
445+
Args:
446+
sampler: The quantum engine or simulator to run the circuits.
447+
qubits: Qubits under test. If none, uses all qubits on the sampler's device.
448+
entangling_gate: The entangling gate to use.
449+
n_repetitions: The number of repetitions to use.
450+
n_combinations: The number of combinations to generate.
451+
n_circuits: The number of circuits to generate.
452+
cycle_depths: The cycle depths to use.
453+
random_state: The random state to use.
454+
ax: the plt.Axes to plot the device layout on. If not given,
455+
no plot is created.
456+
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
457+
Returns:
458+
A TwoQubitXEBResult object representing the results of the experiment.
459+
Raises:
460+
ValueError: If qubits are not specified and the sampler has no device.
461+
"""
462+
fids, *_ = parallel_xeb_workflow(
463+
sampler=sampler,
464+
qubits=qubits,
465+
entangling_gate=entangling_gate,
466+
n_repetitions=n_repetitions,
467+
n_combinations=n_combinations,
468+
n_circuits=n_circuits,
469+
cycle_depths=cycle_depths,
470+
random_state=random_state,
471+
ax=ax,
472+
**plot_kwargs,
473+
)
423474
return TwoQubitXEBResult(fit_exponential_decays(fids))
424475

425476

cirq/experiments/xeb_fitting.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,69 @@ def get_initial_simplex_and_names(
146146
"""Return an initial Nelder-Mead simplex and the names for each parameter."""
147147

148148

149+
def _try_defaults_from_unitary(gate: 'cirq.Gate') -> Optional[Dict[str, 'cirq.TParamVal']]:
150+
r"""Try to figure out the PhasedFSim angles from the unitary of the gate.
151+
152+
The unitary of a PhasedFSimGate has the form:
153+
$$
154+
\begin{bmatrix}
155+
1 & 0 & 0 & 0 \\
156+
0 & e^{-i \gamma - i \zeta} \cos(\theta) & -i e^{-i \gamma + i\chi} \sin(\theta) & 0 \\
157+
0 & -i e^{-i \gamma - i \chi} \sin(\theta) & e^{-i \gamma + i \zeta} \cos(\theta) & 0 \\
158+
0 & 0 & 0 & e^{-2i \gamma - i \phi}
159+
\end{bmatrix}
160+
$$
161+
That's the information about the five angles $\theta, \phi, \gamma, \zeta, \chi$ is encoded in
162+
the submatrix unitary[1:3, 1:3] and the element u[3][3]. With some algebra, we can isolate each
163+
of the angles as an argument of a combination of those elements (and potentially other angles).
164+
165+
Args:
166+
A cirq gate.
167+
168+
Returns:
169+
A dictionary mapping angles to values or None if the gate doesn't have a unitary or if it
170+
can't be represented by a PhasedFSimGate.
171+
"""
172+
u = protocols.unitary(gate, default=None)
173+
if u is None:
174+
return None
175+
176+
gamma = np.angle(u[1, 1] * u[2, 2] - u[1, 2] * u[2, 1]) / -2
177+
phi = -np.angle(u[3, 3]) - 2 * gamma
178+
phased_cos_theta_2 = u[1, 1] * u[2, 2]
179+
if phased_cos_theta_2 == 0:
180+
# The zeta phase is multiplied with cos(theta),
181+
# so if cos(theta) is zero then any value is possible.
182+
zeta = 0
183+
else:
184+
zeta = np.angle(u[2, 2] / u[1, 1]) / 2
185+
186+
phased_sin_theta_2 = u[1, 2] * u[2, 1]
187+
if phased_sin_theta_2 == 0:
188+
# The chi phase is multiplied with sin(theta),
189+
# so if sin(theta) is zero then any value is possible.
190+
chi = 0
191+
else:
192+
chi = np.angle(u[1, 2] / u[2, 1]) / 2
193+
194+
theta = np.angle(np.exp(1j * (gamma + zeta)) * u[1, 1] - np.exp(1j * (gamma - chi)) * u[1, 2])
195+
196+
if np.allclose(
197+
u,
198+
protocols.unitary(
199+
ops.PhasedFSimGate(theta=theta, phi=phi, chi=chi, zeta=zeta, gamma=gamma)
200+
),
201+
):
202+
return {
203+
'theta_default': theta,
204+
'phi_default': phi,
205+
'gamma_default': gamma,
206+
'zeta_default': zeta,
207+
'chi_default': chi,
208+
}
209+
return None
210+
211+
149212
def phased_fsim_angles_from_gate(gate: 'cirq.Gate') -> Dict[str, 'cirq.TParamVal']:
150213
"""For a given gate, return a dictionary mapping '{angle}_default' to its noiseless value
151214
for the five PhasedFSim angles."""
@@ -175,6 +238,11 @@ def phased_fsim_angles_from_gate(gate: 'cirq.Gate') -> Dict[str, 'cirq.TParamVal
175238
'phi_default': gate.phi,
176239
}
177240

241+
# Handle all gates that can be represented using an FSimGate.
242+
from_unitary = _try_defaults_from_unitary(gate)
243+
if from_unitary is not None:
244+
return from_unitary
245+
178246
raise ValueError(f"Unknown default angles for {gate}.")
179247

180248

@@ -580,15 +648,6 @@ def _fit_exponential_decay(
580648
return a, layer_fid, a_std, layer_fid_std
581649

582650

583-
def _one_unique(df, name, default):
584-
"""Helper function to assert that there's one unique value in a column and return it."""
585-
if name not in df.columns:
586-
return default
587-
vals = df[name].unique()
588-
assert len(vals) == 1, name
589-
return vals[0]
590-
591-
592651
def fit_exponential_decays(fidelities_df: pd.DataFrame) -> pd.DataFrame:
593652
"""Fit exponential decay curves to a fidelities DataFrame.
594653

cirq/experiments/xeb_fitting_test.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
fit_exponential_decays,
3333
before_and_after_characterization,
3434
XEBPhasedFSimCharacterizationOptions,
35+
phased_fsim_angles_from_gate,
3536
)
3637
from cirq.experiments.xeb_sampling import sample_2q_xeb_circuits
3738

@@ -354,7 +355,7 @@ def test_options_with_defaults_from_gate():
354355
assert options.zeta_default == 0.0
355356

356357
with pytest.raises(ValueError):
357-
_ = XEBPhasedFSimCharacterizationOptions().with_defaults_from_gate(cirq.CZ)
358+
_ = XEBPhasedFSimCharacterizationOptions().with_defaults_from_gate(cirq.XX)
358359

359360

360361
def test_options_defaults_set():
@@ -395,3 +396,34 @@ def test_options_defaults_set():
395396
phi_default=0.0,
396397
)
397398
assert o3.defaults_set() is True
399+
400+
401+
def _random_angles(n, seed):
402+
rng = np.random.default_rng(seed)
403+
r = 2 * rng.random((n, 5)) - 1
404+
return np.pi * r
405+
406+
407+
@pytest.mark.parametrize(
408+
'gate',
409+
[
410+
cirq.CZ,
411+
cirq.SQRT_ISWAP,
412+
cirq.SQRT_ISWAP_INV,
413+
cirq.ISWAP,
414+
cirq.ISWAP_INV,
415+
cirq.cphase(0.1),
416+
cirq.CZ**0.2,
417+
]
418+
+ [cirq.PhasedFSimGate(*r) for r in _random_angles(10, 0)],
419+
)
420+
def test_phased_fsim_angles_from_gate(gate):
421+
angles = phased_fsim_angles_from_gate(gate)
422+
angles = {k.removesuffix('_default'): v for k, v in angles.items()}
423+
phasedfsim = cirq.PhasedFSimGate(**angles)
424+
np.testing.assert_allclose(cirq.unitary(phasedfsim), cirq.unitary(gate), atol=1e-9)
425+
426+
427+
def test_phased_fsim_angles_from_gate_unsupporet_gate():
428+
with pytest.raises(ValueError, match='Unknown default angles'):
429+
_ = phased_fsim_angles_from_gate(cirq.testing.TwoQubitGate())

0 commit comments

Comments
 (0)