Skip to content

Commit f65e3e9

Browse files
authored
Convert ASV benchmarks to pytest-benchmark (#7799)
This duplicates ASV benchmarks as follows: - `circuit_construction.py` to `circuit_construction_perf.py` - `bench_linalg_decompositions.py` to `linalg_decompositions_perf.py` - `parameter_resolution.py` to `parameter_resolution_perf.py` - `randomized_benchmarking.py` to `randomized_benchmarking_perf.py` - `serialization.py` to `serialization_perf.py` - `transformers/routing.py` to `transformers/routing_perf.py` - `transformers/transformer_primitives.py` to `transformers/transformer_primitives_perf.py` Partially implements #7796
1 parent 6147746 commit f65e3e9

File tree

8 files changed

+542
-0
lines changed

8 files changed

+542
-0
lines changed

asv.conf.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"build_command": [
88
"PIP_NO_BUILD_ISOLATION=false python -m pip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}"
99
],
10+
"install_command": ["in-dir={env_dir} python -m pip install {wheel_file} pytest"],
1011
"branches": ["main"],
1112
"dvcs": "git",
1213
"environment_type": "virtualenv",
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Copyright 2026 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.
14+
15+
"""Performance tests for circuit construction."""
16+
17+
import itertools
18+
from collections.abc import Sequence
19+
20+
import pandas
21+
import pytest
22+
23+
import cirq
24+
25+
26+
def rotated_surface_code_memory_z_cycle(
27+
data_qubits: set[cirq.GridQubit],
28+
z_measure_qubits: set[cirq.GridQubit],
29+
x_measure_qubits: set[cirq.GridQubit],
30+
z_order: Sequence[tuple[int, int]],
31+
x_order: Sequence[tuple[int, int]],
32+
) -> cirq.Circuit:
33+
"""Constructs a circuit for a single round of rotated memory Z surface code.
34+
35+
Args:
36+
data_qubits: data qubits for the surface code patch.
37+
z_measure_qubits: measure qubits to measure Z stabilizers for surface code patch.
38+
x_measure_qubits: measure qubits to measure X stabilizers for surface code patch.
39+
z_order: Specifies the order in which the 2/4 data qubit neighbours of a Z measure qubit
40+
should be processed.
41+
x_order: Specifies the order in which the 2/4 data qubit neighbours of a X measure qubit
42+
should be processed.
43+
44+
Returns:
45+
A `cirq.Circuit` for a single round of rotated memory Z surface code cycle.
46+
"""
47+
48+
circuit = cirq.Circuit()
49+
circuit += cirq.Moment([cirq.H(q) for q in x_measure_qubits])
50+
for k in range(4):
51+
op_list = []
52+
for measure_qubits, add, is_x in [
53+
(x_measure_qubits, x_order[k], True),
54+
(z_measure_qubits, z_order[k], False),
55+
]:
56+
for q_meas in measure_qubits:
57+
q_data = q_meas + add
58+
if q_data in data_qubits:
59+
op_list.append(cirq.CNOT(q_meas, q_data) if is_x else cirq.CNOT(q_data, q_meas))
60+
circuit += cirq.Moment(op_list)
61+
circuit += cirq.Moment([cirq.H(q) for q in x_measure_qubits])
62+
circuit += cirq.Moment(cirq.measure_each(*x_measure_qubits, *z_measure_qubits))
63+
return circuit
64+
65+
66+
def surface_code_circuit(
67+
distance: int, num_rounds: int, moment_by_moment: bool = True
68+
) -> cirq.Circuit:
69+
"""Constructs a rotated memory Z surface code circuit with `distance` and `num_rounds`.
70+
71+
The circuit has `dxd` data qubits and `d ** 2 - 1` measure qubits, where `d` is the distance
72+
of surface code. For more details on rotated surface codes and qubit indexing, see figure 13
73+
https://arxiv.org/abs/1111.4022.
74+
75+
Args:
76+
distance: Distance of the surface code.
77+
num_rounds: Number of error correction rounds for memory Z experiment.
78+
moment_by_moment: If True, the circuit is constructed moment-by-moment instead of
79+
operation-by-operation. This is useful to benchmark different circuit construction
80+
patterns for the same circuit.
81+
82+
Returns:
83+
A `cirq.Circuit` for surface code memory Z experiment for `distance` and `num_rounds`.
84+
"""
85+
86+
def ndrange(*ranges: tuple[int, ...]):
87+
return itertools.product(*[range(*r) for r in ranges])
88+
89+
data_qubits = {cirq.q(2 * x + 1, 2 * y + 1) for x, y in ndrange((distance,), (distance,))}
90+
z_measure_qubits = {
91+
cirq.q(2 * x, 2 * y) for x, y in ndrange((1, distance), (distance + 1,)) if x % 2 != y % 2
92+
}
93+
x_measure_qubits = {
94+
cirq.q(2 * x, 2 * y) for x, y in ndrange((distance + 1,), (1, distance)) if x % 2 == y % 2
95+
}
96+
x_order = [(1, 1), (1, -1), (-1, 1), (-1, -1)]
97+
z_order = [(1, 1), (-1, 1), (1, -1), (-1, -1)]
98+
surface_code_cycle = rotated_surface_code_memory_z_cycle(
99+
data_qubits, x_measure_qubits, z_measure_qubits, x_order, z_order
100+
)
101+
if moment_by_moment:
102+
return cirq.Circuit(
103+
surface_code_cycle * num_rounds, cirq.Moment(cirq.measure_each(*data_qubits))
104+
)
105+
else:
106+
return cirq.Circuit(
107+
[*surface_code_cycle.all_operations()] * num_rounds, cirq.measure_each(*data_qubits)
108+
)
109+
110+
111+
class TestSurfaceCodeRotatedMemoryZ:
112+
"""Surface Code Rotated Memory-Z Benchmarks."""
113+
114+
group = "circuit_construction"
115+
expected = pandas.DataFrame.from_dict(
116+
{
117+
3: [64, 369],
118+
5: [176, 3225],
119+
7: [344, 12985],
120+
9: [568, 36369],
121+
11: [848, 82401],
122+
13: [1184, 162409],
123+
15: [1576, 290025],
124+
17: [2024, 481185],
125+
19: [2528, 754129],
126+
21: [3088, 1129401],
127+
23: [3704, 1629849],
128+
25: [4376, 2280625],
129+
},
130+
columns=["depth", "operation_count"],
131+
orient="index",
132+
)
133+
134+
@pytest.mark.parametrize("distance", expected.index)
135+
@pytest.mark.benchmark(group=group)
136+
def test_circuit_construction_moment_by_moment(self, benchmark, distance: int) -> None:
137+
"""Benchmark circuit construction for Rotated Bottom-Z Surface code."""
138+
circuit = benchmark(
139+
surface_code_circuit, distance, num_rounds=distance * distance, moment_by_moment=True
140+
)
141+
assert len(circuit) == self.expected.depth[distance]
142+
143+
@pytest.mark.parametrize("distance", expected.index)
144+
@pytest.mark.benchmark(group=group)
145+
def test_circuit_construction_operations_by_operation(self, benchmark, distance: int) -> None:
146+
"""Benchmark circuit construction for Rotated Bottom-Z Surface code."""
147+
circuit = benchmark(
148+
surface_code_circuit, distance, num_rounds=distance * distance, moment_by_moment=False
149+
)
150+
assert sum(1 for _ in circuit.all_operations()) == self.expected.operation_count[distance]
151+
152+
153+
class TestXOnAllQubitsCircuit:
154+
"""N * D times X gate on all qubits."""
155+
156+
group = "circuit_operations"
157+
158+
@pytest.mark.parametrize(
159+
["qubit_count", "depth"], itertools.product([1, 10, 100, 1000], [1, 10, 100, 1000])
160+
)
161+
@pytest.mark.benchmark(group=group)
162+
def test_circuit_construction(self, benchmark, qubit_count: int, depth: int) -> None:
163+
q = cirq.LineQubit.range(qubit_count)
164+
f = lambda: cirq.Circuit(cirq.Moment(cirq.X.on_each(*q)) for _ in range(depth))
165+
circuit = benchmark(f)
166+
assert len(circuit) == depth
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2026 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.
14+
15+
import pytest
16+
17+
import cirq
18+
19+
20+
@pytest.mark.parametrize(
21+
"gate", [cirq.IdentityGate(2), cirq.SWAP, cirq.ISWAP, cirq.CZ, cirq.CNOT], ids=str
22+
)
23+
@pytest.mark.benchmark(group="linalg_decompositions")
24+
def test_kak_decomposition(benchmark, gate: cirq.Gate) -> None:
25+
"""Benchmark kak_decomposition."""
26+
benchmark(cirq.kak_decomposition, gate)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2026 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.
14+
15+
import itertools
16+
import random
17+
18+
import numpy as np
19+
import pytest
20+
import sympy
21+
22+
import cirq
23+
24+
25+
@pytest.mark.parametrize(
26+
["num_qubits", "num_scan_points"], itertools.product([50, 100, 150, 200], [20, 40, 60, 80, 100])
27+
)
28+
@pytest.mark.benchmark(group="parameter_resolution")
29+
def test_parameter_resolution(benchmark, num_qubits: int, num_scan_points: int) -> None:
30+
qubits = cirq.GridQubit.rect(1, num_qubits)
31+
symbols = {q: sympy.Symbol(f'a_{q}') for q in qubits}
32+
circuit = cirq.Circuit([cirq.X(q) ** symbols[q] for q in qubits], cirq.measure_each(*qubits))
33+
qubit_amps = {q: random.uniform(0.48, 0.52) for q in qubits}
34+
diff_amps = np.linspace(-0.3, 0.3, num=num_scan_points)
35+
36+
def _f():
37+
for diff in diff_amps:
38+
resolver = {symbols[q]: amp + diff for q, amp in qubit_amps.items()}
39+
_ = cirq.resolve_parameters(circuit, resolver)
40+
41+
benchmark(_f)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright 2026 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.
14+
15+
import functools
16+
import itertools
17+
from collections.abc import Iterable, Sequence
18+
19+
import numpy as np
20+
import pytest
21+
22+
import cirq
23+
from cirq.experiments.qubit_characterizations import _find_inv_matrix, _single_qubit_cliffords
24+
25+
26+
def dot(args: Iterable[np.ndarray]) -> np.ndarray:
27+
return functools.reduce(np.dot, args)
28+
29+
30+
class TestSingleQubitRandomizedBenchmarking:
31+
"""Benchmarks circuit construction time for single qubit randomized benchmarking circuits.
32+
33+
Given a combination of `depth`, `num_qubits` and `num_circuits`, the benchmark constructs
34+
`num_circuits` different circuits, each spanning `num_qubits` and containing `depth` moments.
35+
Each moment of the circuit contains a single qubit clifford operation for each qubit.
36+
37+
Thus, the generated circuits have `depth * num_qubits` single qubit clifford operations.
38+
"""
39+
40+
group = "randomized_benchmarking"
41+
depth = [1, 10, 50, 100, 250, 500, 1000]
42+
num_qubits = [100]
43+
num_circuits = [20]
44+
45+
# assigned in setup_class
46+
sq_xz_matrices: np.ndarray
47+
sq_xz_cliffords: Sequence[cirq.Gate]
48+
49+
@classmethod
50+
def setup_class(cls):
51+
cls.sq_xz_matrices = np.array(
52+
[
53+
dot([cirq.unitary(c) for c in reversed(group)])
54+
for group in _single_qubit_cliffords().c1_in_xz
55+
]
56+
)
57+
cls.sq_xz_cliffords = [cirq.PhasedXZGate.from_matrix(mat) for mat in cls.sq_xz_matrices]
58+
59+
def _get_op_grid(self, qubits: Sequence[cirq.Qid], depth: int) -> list[list[cirq.Operation]]:
60+
op_grid: list[list[cirq.Operation]] = []
61+
for q in qubits:
62+
gate_ids = np.random.choice(len(self.sq_xz_cliffords), depth)
63+
idx = _find_inv_matrix(dot(self.sq_xz_matrices[gate_ids][::-1]), self.sq_xz_matrices)
64+
op_sequence = [self.sq_xz_cliffords[gate_id].on(q) for gate_id in gate_ids]
65+
op_sequence.append(self.sq_xz_cliffords[idx].on(q))
66+
op_grid.append(op_sequence)
67+
return op_grid
68+
69+
@pytest.mark.parametrize(
70+
["depth", "num_qubits", "num_circuits"], itertools.product(depth, num_qubits, num_circuits)
71+
)
72+
@pytest.mark.benchmark(group=group)
73+
def test_rb_op_grid_generation(
74+
self, benchmark, depth: int, num_qubits: int, num_circuits: int
75+
) -> None:
76+
qubits = cirq.GridQubit.rect(1, num_qubits)
77+
78+
def _f() -> None:
79+
for _ in range(num_circuits):
80+
self._get_op_grid(qubits, depth)
81+
82+
benchmark(_f)
83+
84+
@pytest.mark.parametrize(
85+
["depth", "num_qubits", "num_circuits"], itertools.product(depth, num_qubits, num_circuits)
86+
)
87+
@pytest.mark.benchmark(group=group)
88+
def test_rb_circuit_construction(
89+
self, benchmark, depth: int, num_qubits: int, num_circuits: int
90+
) -> None:
91+
qubits = cirq.GridQubit.rect(1, num_qubits)
92+
93+
def _f() -> None:
94+
for _ in range(num_circuits):
95+
op_grid = self._get_op_grid(qubits, depth)
96+
cirq.Circuit(
97+
[cirq.Moment(ops[d] for ops in op_grid) for d in range(depth + 1)],
98+
cirq.Moment(cirq.measure(*qubits)),
99+
)
100+
101+
benchmark(_f)

0 commit comments

Comments
 (0)