diff --git a/cirq-core/cirq/experiments/__init__.py b/cirq-core/cirq/experiments/__init__.py index 21df19acda1..8df1351d500 100644 --- a/cirq-core/cirq/experiments/__init__.py +++ b/cirq-core/cirq/experiments/__init__.py @@ -15,12 +15,15 @@ from cirq.experiments.qubit_characterizations import ( RandomizedBenchMarkResult as RandomizedBenchMarkResult, + RBParameters as RBParameters, single_qubit_randomized_benchmarking as single_qubit_randomized_benchmarking, + single_qubit_rb as single_qubit_rb, single_qubit_state_tomography as single_qubit_state_tomography, TomographyResult as TomographyResult, two_qubit_randomized_benchmarking as two_qubit_randomized_benchmarking, two_qubit_state_tomography as two_qubit_state_tomography, parallel_single_qubit_randomized_benchmarking as parallel_single_qubit_randomized_benchmarking, + parallel_single_qubit_rb as parallel_single_qubit_rb, ) from cirq.experiments.fidelity_estimation import ( diff --git a/cirq-core/cirq/experiments/qubit_characterizations.py b/cirq-core/cirq/experiments/qubit_characterizations.py index 5dbc22e9a6e..6da94586dfb 100644 --- a/cirq-core/cirq/experiments/qubit_characterizations.py +++ b/cirq-core/cirq/experiments/qubit_characterizations.py @@ -19,6 +19,7 @@ import itertools from typing import Any, cast, Iterator, Mapping, Sequence, TYPE_CHECKING +import attrs import numpy as np from matplotlib import pyplot as plt @@ -28,6 +29,7 @@ import cirq.vis.heatmap as cirq_heatmap import cirq.vis.histogram as cirq_histogram from cirq import circuits, ops, protocols +from cirq._compat import deprecated from cirq.devices import grid_qubit if TYPE_CHECKING: @@ -36,6 +38,12 @@ import cirq +def _canonize_clifford_sequences( + sequences: list[list[ops.SingleQubitCliffordGate]], +) -> list[list[ops.SingleQubitCliffordGate]]: + return [[_reduce_gate_seq(seq)] for seq in sequences] + + @dataclasses.dataclass class Cliffords: """The single-qubit Clifford group, decomposed into elementary gates. @@ -134,7 +142,7 @@ def _fit_exponential(self) -> tuple[np.ndarray, np.ndarray]: xdata=self._num_cfds_seq, ydata=self._gnd_state_probs, p0=[0.5, 0.5, 1.0 - 1e-3], - bounds=([0, 0.25, 0], [0.5, 0.75, 1]), + bounds=([0, -1, 0], [1, 1, 1]), ) @@ -333,6 +341,44 @@ def plot( return axes +@attrs.frozen +class RBParameters: + r"""Parameters for running randomized benchmarking. + + Arguments: + num_clifford_range: The different numbers of Cliffords in the RB study. + num_circuits: The number of random circuits generated for each + number of Cliffords. + repetitions: The number of repetitions of each circuit. + use_xy_basis: Determines if the Clifford gates are built with x and y + rotations (True) or x and z rotations (False). + strict_basis: whether to use only cliffords that can be represented by at + most 2 gates of the choses basis. For example, + if True and use_xy_basis is True, this excludes $I, Z, \sqrt(Z), \-sqrt(Z)^\dagger$. + if True and use_xy_basis is False, this excludes $I, Y, \sqrt(Y), -\sqrt(Y)^\dagger$. + """ + + num_clifford_range: Sequence[int] = tuple(np.logspace(np.log10(5), 3, 5, dtype=int)) + num_circuits: int = 10 + repetitions: int = 600 + use_xy_basis: bool = False + strict_basis: bool = True + + def gateset(self) -> list[list[ops.SingleQubitCliffordGate]]: + clifford_group = _single_qubit_cliffords() + sequences = clifford_group.c1_in_xy if self.use_xy_basis else clifford_group.c1_in_xz + sequences = _canonize_clifford_sequences(sequences) + if self.strict_basis: + if self.use_xy_basis: + excluded_gates = ops.Gateset(ops.I, ops.Z, ops.Z**0.5, ops.Z**-0.5) + else: + excluded_gates = ops.Gateset(ops.I, ops.Y, ops.Y**0.5, ops.Y**-0.5) + + sequences = [[g] for (g,) in sequences if g not in excluded_gates] + return sequences + + +@deprecated(deadline='v2.0', fix='please use single_qubit_rb instead') def single_qubit_randomized_benchmarking( sampler: cirq.Sampler, qubit: cirq.Qid, @@ -376,17 +422,20 @@ def single_qubit_randomized_benchmarking( A RandomizedBenchMarkResult object that stores and plots the result. """ - result = parallel_single_qubit_randomized_benchmarking( + return single_qubit_rb( sampler, - (qubit,), - use_xy_basis, - num_clifford_range=num_clifford_range, - num_circuits=num_circuits, - repetitions=repetitions, + qubit, + RBParameters( + num_clifford_range=num_clifford_range, + num_circuits=num_circuits, + repetitions=repetitions, + use_xy_basis=use_xy_basis, + strict_basis=False, + ), ) - return result.results_dictionary[qubit] +@deprecated(deadline='v2.0', fix='please use parallel_single_qubit_rb instead') def parallel_single_qubit_randomized_benchmarking( sampler: cirq.Sampler, qubits: Sequence[cirq.Qid], @@ -413,35 +462,90 @@ def parallel_single_qubit_randomized_benchmarking( num_circuits: The number of random circuits generated for each number of Cliffords. repetitions: The number of repetitions of each circuit. + Returns: + A dictionary from qubits to RandomizedBenchMarkResult objects. + """ + return parallel_single_qubit_rb( + sampler, + qubits, + RBParameters( + num_clifford_range=num_clifford_range, + num_circuits=num_circuits, + repetitions=repetitions, + use_xy_basis=use_xy_basis, + strict_basis=False, + ), + ) + + +def single_qubit_rb( + sampler: cirq.Sampler, + qubit: cirq.Qid, + parameters: RBParameters = RBParameters(), + rng_or_seed: np.random.Generator | int | None = None, +) -> RandomizedBenchMarkResult: + """Clifford-based randomized benchmarking (RB) on a single qubit. + + Args: + sampler: The quantum engine or simulator to run the circuits. + qubit: The qubit(s) to benchmark. + parameters: The parameters of the experiment. + rng_or_seed: A np.random.Generator object or seed. + Returns: + A dictionary from qubits to RandomizedBenchMarkResult objects. + """ + return parallel_single_qubit_rb(sampler, [qubit], parameters, rng_or_seed).results_dictionary[ + qubit + ] + + +def parallel_single_qubit_rb( + sampler: cirq.Sampler, + qubits: Sequence[cirq.Qid], + parameters: RBParameters = RBParameters(), + rng_or_seed: np.random.Generator | int | None = None, +) -> ParallelRandomizedBenchmarkingResult: + """Clifford-based randomized benchmarking (RB) single qubits in parallel. + Args: + sampler: The quantum engine or simulator to run the circuits. + qubits: The qubit(s) to benchmark. + parameters: The parameters of the experiment. + rng_or_seed: A np.random.Generator object or seed. Returns: A dictionary from qubits to RandomizedBenchMarkResult objects. """ - clifford_group = _single_qubit_cliffords() - c1 = clifford_group.c1_in_xy if use_xy_basis else clifford_group.c1_in_xz + rng_or_seed = ( + rng_or_seed + if isinstance(rng_or_seed, np.random.Generator) + else np.random.default_rng(rng_or_seed) + ) + + c1 = parameters.gateset() # create circuits circuits_all: list[cirq.AbstractCircuit] = [] - for num_cliffords in num_clifford_range: - for _ in range(num_circuits): - circuits_all.append(_create_parallel_rb_circuit(qubits, num_cliffords, c1)) + for num_cliffords in parameters.num_clifford_range: + for _ in range(parameters.num_circuits): + circuits_all.append(_create_parallel_rb_circuit(qubits, num_cliffords, c1, rng_or_seed)) # run circuits - results = sampler.run_batch(circuits_all, repetitions=repetitions) + results = sampler.run_batch(circuits_all, repetitions=parameters.repetitions) gnd_probs: dict = {q: [] for q in qubits} idx = 0 - for num_cliffords in num_clifford_range: + for num_cliffords in parameters.num_clifford_range: excited_probs: dict[cirq.Qid, list[float]] = {q: [] for q in qubits} - for _ in range(num_circuits): + for _ in range(parameters.num_circuits): result = results[idx][0] for qubit in qubits: excited_probs[qubit].append(np.mean(result.measurements[str(qubit)])) idx += 1 for qubit in qubits: gnd_probs[qubit].append(1.0 - np.mean(excited_probs[qubit])) + return ParallelRandomizedBenchmarkingResult( - {q: RandomizedBenchMarkResult(num_clifford_range, gnd_probs[q]) for q in qubits} + {q: RandomizedBenchMarkResult(parameters.num_clifford_range, gnd_probs[q]) for q in qubits} ) @@ -677,9 +781,14 @@ def _measurement(two_qubit_circuit: circuits.Circuit) -> np.ndarray: def _create_parallel_rb_circuit( - qubits: Sequence[cirq.Qid], num_cliffords: int, c1: list + qubits: Sequence[cirq.Qid], + num_cliffords: int, + c1: list[list[ops.SingleQubitCliffordGate]], + rng: np.random.Generator | None = None, ) -> cirq.Circuit: - sequences_to_zip = [_random_single_q_clifford(qubit, num_cliffords, c1) for qubit in qubits] + sequences_to_zip = [ + _random_single_q_clifford(qubit, num_cliffords, c1, rng) for qubit in qubits + ] # Ensure each sequence has the same number of moments. num_moments = max(len(sequence) for sequence in sequences_to_zip) for q, sequence in zip(qubits, sequences_to_zip): @@ -730,11 +839,14 @@ def _two_qubit_clifford_matrices(q_0: cirq.Qid, q_1: cirq.Qid, cliffords: Cliffo def _random_single_q_clifford( - qubit: cirq.Qid, num_cfds: int, cfds: Sequence[Sequence[cirq.ops.SingleQubitCliffordGate]] + qubit: cirq.Qid, + num_cfds: int, + cfds: Sequence[Sequence[cirq.ops.SingleQubitCliffordGate]], + rng: np.random.Generator | None = None, ) -> list[cirq.Operation]: - clifford_group_size = 24 operations = [[gate.to_phased_xz_gate()(qubit) for gate in gates] for gates in cfds] - gate_ids = np.random.choice(clifford_group_size, num_cfds).tolist() + choice_fn = rng.choice if rng else np.random.choice + gate_ids = choice_fn(len(cfds), num_cfds).tolist() adjoint = _reduce_gate_seq([gate for gate_id in gate_ids for gate in cfds[gate_id]]) ** -1 return [op for gate_id in gate_ids for op in operations[gate_id]] + [ adjoint.to_phased_xz_gate()(qubit) diff --git a/cirq-core/cirq/experiments/qubit_characterizations_test.py b/cirq-core/cirq/experiments/qubit_characterizations_test.py index 6e792c16477..9f4c9e6569e 100644 --- a/cirq-core/cirq/experiments/qubit_characterizations_test.py +++ b/cirq-core/cirq/experiments/qubit_characterizations_test.py @@ -14,6 +14,9 @@ from __future__ import annotations +import os +from unittest import mock + import matplotlib.pyplot as plt import numpy as np import pytest @@ -104,6 +107,7 @@ def check_distinct(unitaries): assert num_x <= 1 +@mock.patch.dict(os.environ, clear='CIRQ_TESTING') def test_single_qubit_randomized_benchmarking(): # Check that the ground state population at the end of the Clifford # sequences is always unity. @@ -116,7 +120,8 @@ def test_single_qubit_randomized_benchmarking(): assert np.isclose(results.pauli_error(), 0.0, atol=1e-7) # warning is expected -def test_parallel_single_qubit_randomized_benchmarking(): +@mock.patch.dict(os.environ, clear='CIRQ_TESTING') +def test_parallel_single_qubit_parallel_single_qubit_randomized_benchmarking(): # Check that the ground state population at the end of the Clifford # sequences is always unity. simulator = sim.Simulator() @@ -229,13 +234,24 @@ def test_tomography_plot_raises_for_incorrect_number_of_axes(): result.plot(axes) -def test_single_qubit_cliffords_gateset(): +@pytest.mark.parametrize('num_cliffords', range(5, 10)) +@pytest.mark.parametrize('use_xy_basis', [False, True]) +@pytest.mark.parametrize('strict_basis', [False, True]) +def test_single_qubit_cliffords_gateset(num_cliffords, use_xy_basis, strict_basis): qubits = [GridQubit(0, i) for i in range(4)] - clifford_group = cirq.experiments.qubit_characterizations._single_qubit_cliffords() + c1_in_xy = cirq.experiments.qubit_characterizations.RBParameters( + use_xy_basis=use_xy_basis, strict_basis=strict_basis + ).gateset() + if strict_basis: + assert len(c1_in_xy) == 20 + else: + assert len(c1_in_xy) == 24 c = cirq.experiments.qubit_characterizations._create_parallel_rb_circuit( - qubits, 5, clifford_group.c1_in_xy + qubits, num_cliffords, c1_in_xy ) device = cirq.testing.ValidatingTestDevice( qubits=qubits, allowed_gates=(cirq.ops.PhasedXZGate, cirq.MeasurementGate) ) device.validate_circuit(c) + + assert len(c) == num_cliffords + 2 diff --git a/cirq-core/cirq/experiments/two_qubit_xeb.py b/cirq-core/cirq/experiments/two_qubit_xeb.py index fdd85a6647e..149edcfcd64 100644 --- a/cirq-core/cirq/experiments/two_qubit_xeb.py +++ b/cirq-core/cirq/experiments/two_qubit_xeb.py @@ -30,8 +30,9 @@ from cirq._compat import cached_method from cirq.experiments import random_quantum_circuit_generation as rqcg from cirq.experiments.qubit_characterizations import ( - parallel_single_qubit_randomized_benchmarking, + parallel_single_qubit_rb, ParallelRandomizedBenchmarkingResult, + RBParameters, ) from cirq.experiments.xeb_fitting import ( benchmark_2q_xeb_fidelities, @@ -586,12 +587,14 @@ def run_rb_and_xeb( qubits, pairs = qubits_and_pairs(sampler, qubits, pairs) - rb = parallel_single_qubit_randomized_benchmarking( + rb = parallel_single_qubit_rb( sampler=sampler, qubits=qubits, - repetitions=repetitions, - num_circuits=num_circuits, - num_clifford_range=num_clifford_range, + parameters=RBParameters( + num_circuits=num_circuits, + repetitions=repetitions, + num_clifford_range=num_clifford_range, + ), ) xeb = parallel_two_qubit_xeb( diff --git a/examples/qubit_characterizations_example.py b/examples/qubit_characterizations_example.py index 6153a0c63dd..38e265e6217 100644 --- a/examples/qubit_characterizations_example.py +++ b/examples/qubit_characterizations_example.py @@ -34,8 +34,10 @@ def main(minimum_cliffords=5, maximum_cliffords=20, cliffords_step=5): clifford_range = range(minimum_cliffords, maximum_cliffords, cliffords_step) # Clifford-based randomized benchmarking of single-qubit gates on q_0. - rb_result_1q = cirq.experiments.single_qubit_randomized_benchmarking( - simulator, q_0, num_clifford_range=clifford_range, repetitions=100 + rb_result_1q = cirq.experiments.single_qubit_rb( + simulator, + q_0, + cirq.experiments.RBParameters(num_clifford_range=clifford_range, repetitions=100), ) rb_result_1q.plot()