Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions cirq-core/cirq/experiments/qubit_characterizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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.
Expand Down Expand Up @@ -336,7 +342,7 @@ def plot(
def single_qubit_randomized_benchmarking(
sampler: cirq.Sampler,
qubit: cirq.Qid,
use_xy_basis: bool = True,
use_xy_basis: bool = False,
Copy link
Collaborator

@pavoljuhas pavoljuhas Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That interface has been around since #1533. Is there enough benefit from the new default to risk breaking any callers that expect the old value?

Also we have some callers of single_qubit_randomized_benchmarking and parallel_single_qubit_randomized_benchmarking in cirq sources that do not pass use_xy_basis.
Would they still work as intended?

I suggest to leave the default as is in this PR and if there is a strong argument for its change, do so in a separate PR (and label it as "BREAKING CHANGE").

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to me, it is fine either way. @eliottrosenberg what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely change this (as Nour does here) in order to match the default behavior of the internal implementation of RB. I don't have a preference as to whether it is in this PR or a different one. @pavoljuhas I doubt that a lot of people are using this, so I'm not worried about breaking changes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eliottrosenberg we decided to create a new method to run RB that has the correct default values and deprecate the old methods.

Copy link
Collaborator

@eliottrosenberg eliottrosenberg Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is probably overkill, but if you insist, it's ok.

*,
num_clifford_range: Sequence[int] = tuple(np.logspace(np.log10(5), 3, 5, dtype=int)),
num_circuits: int = 10,
Expand Down Expand Up @@ -387,16 +393,29 @@ def single_qubit_randomized_benchmarking(
return result.results_dictionary[qubit]


def _gateset_selector(
use_xy_basis: bool, xy_only: bool, xz_only: bool
) -> list[list[ops.SingleQubitCliffordGate]]:
clifford_group = _single_qubit_cliffords()
sequences = clifford_group.c1_in_xy if use_xy_basis else clifford_group.c1_in_xz
filter_seq = lambda seq: len(seq) == 2
if xy_only or xz_only:
sequences = list(filter(filter_seq, sequences))
return _canonize_clifford_sequences(sequences)


def parallel_single_qubit_randomized_benchmarking(
sampler: cirq.Sampler,
qubits: Sequence[cirq.Qid],
use_xy_basis: bool = True,
use_xy_basis: bool = False,
*,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of the "*," here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it forces those other parameters to be keyword only

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't make a lot of sense to have use_xy_basis be above the star, I think.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it would be cleaner, but converting use_xy_basis to a keyword-only argument would break any callers that pass it as a positional arg. It is probably not worth the inconvenience.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there are many users of this RB implementation and I'm not worried about breaking old workflows here.

num_clifford_range: Sequence[int] = tuple(
np.logspace(np.log10(5), np.log10(1000), 5, dtype=int)
),
num_circuits: int = 10,
repetitions: int = 1000,
xy_only: bool = True,
xz_only: bool = False,
) -> ParallelRandomizedBenchmarkingResult:
"""Clifford-based randomized benchmarking (RB) single qubits in parallel.

Expand All @@ -413,13 +432,15 @@ 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.

xy_only: whether to use only gates that have either an X or a Y component.
if True, this excludes I, Z, Z/2, -Z/2.
xz_only: whether to use only gates that have either an X or a Z component.
if True, this excludes I, Y, Y/2, -Y/2.
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
c1 = _gateset_selector(use_xy_basis, xy_only, xz_only)

# create circuits
circuits_all: list[cirq.AbstractCircuit] = []
Expand Down Expand Up @@ -677,7 +698,7 @@ 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]]
) -> cirq.Circuit:
sequences_to_zip = [_random_single_q_clifford(qubit, num_cliffords, c1) for qubit in qubits]
# Ensure each sequence has the same number of moments.
Expand Down Expand Up @@ -732,9 +753,8 @@ 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]]
) -> 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()
gate_ids = np.random.choice(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)
Expand Down
14 changes: 11 additions & 3 deletions cirq-core/cirq/experiments/qubit_characterizations_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,21 @@ 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('xy_only', [False, True])
@pytest.mark.parametrize('xz_only', [False, True])
def test_single_qubit_cliffords_gateset(num_cliffords, use_xy_basis, xy_only, xz_only):
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._gateset_selector(
use_xy_basis, xy_only, xz_only
)
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