diff --git a/asv.conf.json b/asv.conf.json index 0febeda3ad6..d55e39cb073 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -7,6 +7,7 @@ "build_command": [ "PIP_NO_BUILD_ISOLATION=false python -m pip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" ], + "install_command": ["in-dir={env_dir} python -m pip install {wheel_file} pytest"], "branches": ["main"], "dvcs": "git", "environment_type": "virtualenv", diff --git a/benchmarks/circuit_construction_perf.py b/benchmarks/circuit_construction_perf.py new file mode 100644 index 00000000000..f9900802b8a --- /dev/null +++ b/benchmarks/circuit_construction_perf.py @@ -0,0 +1,166 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Performance tests for circuit construction.""" + +import itertools +from collections.abc import Sequence + +import pandas +import pytest + +import cirq + + +def rotated_surface_code_memory_z_cycle( + data_qubits: set[cirq.GridQubit], + z_measure_qubits: set[cirq.GridQubit], + x_measure_qubits: set[cirq.GridQubit], + z_order: Sequence[tuple[int, int]], + x_order: Sequence[tuple[int, int]], +) -> cirq.Circuit: + """Constructs a circuit for a single round of rotated memory Z surface code. + + Args: + data_qubits: data qubits for the surface code patch. + z_measure_qubits: measure qubits to measure Z stabilizers for surface code patch. + x_measure_qubits: measure qubits to measure X stabilizers for surface code patch. + z_order: Specifies the order in which the 2/4 data qubit neighbours of a Z measure qubit + should be processed. + x_order: Specifies the order in which the 2/4 data qubit neighbours of a X measure qubit + should be processed. + + Returns: + A `cirq.Circuit` for a single round of rotated memory Z surface code cycle. + """ + + circuit = cirq.Circuit() + circuit += cirq.Moment([cirq.H(q) for q in x_measure_qubits]) + for k in range(4): + op_list = [] + for measure_qubits, add, is_x in [ + (x_measure_qubits, x_order[k], True), + (z_measure_qubits, z_order[k], False), + ]: + for q_meas in measure_qubits: + q_data = q_meas + add + if q_data in data_qubits: + op_list.append(cirq.CNOT(q_meas, q_data) if is_x else cirq.CNOT(q_data, q_meas)) + circuit += cirq.Moment(op_list) + circuit += cirq.Moment([cirq.H(q) for q in x_measure_qubits]) + circuit += cirq.Moment(cirq.measure_each(*x_measure_qubits, *z_measure_qubits)) + return circuit + + +def surface_code_circuit( + distance: int, num_rounds: int, moment_by_moment: bool = True +) -> cirq.Circuit: + """Constructs a rotated memory Z surface code circuit with `distance` and `num_rounds`. + + The circuit has `dxd` data qubits and `d ** 2 - 1` measure qubits, where `d` is the distance + of surface code. For more details on rotated surface codes and qubit indexing, see figure 13 + https://arxiv.org/abs/1111.4022. + + Args: + distance: Distance of the surface code. + num_rounds: Number of error correction rounds for memory Z experiment. + moment_by_moment: If True, the circuit is constructed moment-by-moment instead of + operation-by-operation. This is useful to benchmark different circuit construction + patterns for the same circuit. + + Returns: + A `cirq.Circuit` for surface code memory Z experiment for `distance` and `num_rounds`. + """ + + def ndrange(*ranges: tuple[int, ...]): + return itertools.product(*[range(*r) for r in ranges]) + + data_qubits = {cirq.q(2 * x + 1, 2 * y + 1) for x, y in ndrange((distance,), (distance,))} + z_measure_qubits = { + cirq.q(2 * x, 2 * y) for x, y in ndrange((1, distance), (distance + 1,)) if x % 2 != y % 2 + } + x_measure_qubits = { + cirq.q(2 * x, 2 * y) for x, y in ndrange((distance + 1,), (1, distance)) if x % 2 == y % 2 + } + x_order = [(1, 1), (1, -1), (-1, 1), (-1, -1)] + z_order = [(1, 1), (-1, 1), (1, -1), (-1, -1)] + surface_code_cycle = rotated_surface_code_memory_z_cycle( + data_qubits, x_measure_qubits, z_measure_qubits, x_order, z_order + ) + if moment_by_moment: + return cirq.Circuit( + surface_code_cycle * num_rounds, cirq.Moment(cirq.measure_each(*data_qubits)) + ) + else: + return cirq.Circuit( + [*surface_code_cycle.all_operations()] * num_rounds, cirq.measure_each(*data_qubits) + ) + + +class TestSurfaceCodeRotatedMemoryZ: + """Surface Code Rotated Memory-Z Benchmarks.""" + + group = "circuit_construction" + expected = pandas.DataFrame.from_dict( + { + 3: [64, 369], + 5: [176, 3225], + 7: [344, 12985], + 9: [568, 36369], + 11: [848, 82401], + 13: [1184, 162409], + 15: [1576, 290025], + 17: [2024, 481185], + 19: [2528, 754129], + 21: [3088, 1129401], + 23: [3704, 1629849], + 25: [4376, 2280625], + }, + columns=["depth", "operation_count"], + orient="index", + ) + + @pytest.mark.parametrize("distance", expected.index) + @pytest.mark.benchmark(group=group) + def test_circuit_construction_moment_by_moment(self, benchmark, distance: int) -> None: + """Benchmark circuit construction for Rotated Bottom-Z Surface code.""" + circuit = benchmark( + surface_code_circuit, distance, num_rounds=distance * distance, moment_by_moment=True + ) + assert len(circuit) == self.expected.depth[distance] + + @pytest.mark.parametrize("distance", expected.index) + @pytest.mark.benchmark(group=group) + def test_circuit_construction_operations_by_operation(self, benchmark, distance: int) -> None: + """Benchmark circuit construction for Rotated Bottom-Z Surface code.""" + circuit = benchmark( + surface_code_circuit, distance, num_rounds=distance * distance, moment_by_moment=False + ) + assert sum(1 for _ in circuit.all_operations()) == self.expected.operation_count[distance] + + +class TestXOnAllQubitsCircuit: + """N * D times X gate on all qubits.""" + + group = "circuit_operations" + + @pytest.mark.parametrize( + ["qubit_count", "depth"], itertools.product([1, 10, 100, 1000], [1, 10, 100, 1000]) + ) + @pytest.mark.benchmark(group=group) + def test_circuit_construction(self, benchmark, qubit_count: int, depth: int) -> None: + q = cirq.LineQubit.range(qubit_count) + f = lambda: cirq.Circuit(cirq.Moment(cirq.X.on_each(*q)) for _ in range(depth)) + circuit = benchmark(f) + assert len(circuit) == depth diff --git a/benchmarks/linalg_decompositions_perf.py b/benchmarks/linalg_decompositions_perf.py new file mode 100644 index 00000000000..81b56f0531b --- /dev/null +++ b/benchmarks/linalg_decompositions_perf.py @@ -0,0 +1,26 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import cirq + + +@pytest.mark.parametrize( + "gate", [cirq.IdentityGate(2), cirq.SWAP, cirq.ISWAP, cirq.CZ, cirq.CNOT], ids=str +) +@pytest.mark.benchmark(group="linalg_decompositions") +def test_kak_decomposition(benchmark, gate: cirq.Gate) -> None: + """Benchmark kak_decomposition.""" + benchmark(cirq.kak_decomposition, gate) diff --git a/benchmarks/parameter_resolution_perf.py b/benchmarks/parameter_resolution_perf.py new file mode 100644 index 00000000000..6bbdfae5f11 --- /dev/null +++ b/benchmarks/parameter_resolution_perf.py @@ -0,0 +1,41 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import random + +import numpy as np +import pytest +import sympy + +import cirq + + +@pytest.mark.parametrize( + ["num_qubits", "num_scan_points"], itertools.product([50, 100, 150, 200], [20, 40, 60, 80, 100]) +) +@pytest.mark.benchmark(group="parameter_resolution") +def test_parameter_resolution(benchmark, num_qubits: int, num_scan_points: int) -> None: + qubits = cirq.GridQubit.rect(1, num_qubits) + symbols = {q: sympy.Symbol(f'a_{q}') for q in qubits} + circuit = cirq.Circuit([cirq.X(q) ** symbols[q] for q in qubits], cirq.measure_each(*qubits)) + qubit_amps = {q: random.uniform(0.48, 0.52) for q in qubits} + diff_amps = np.linspace(-0.3, 0.3, num=num_scan_points) + + def _f(): + for diff in diff_amps: + resolver = {symbols[q]: amp + diff for q, amp in qubit_amps.items()} + _ = cirq.resolve_parameters(circuit, resolver) + + benchmark(_f) diff --git a/benchmarks/randomized_benchmarking_perf.py b/benchmarks/randomized_benchmarking_perf.py new file mode 100644 index 00000000000..1aba712513a --- /dev/null +++ b/benchmarks/randomized_benchmarking_perf.py @@ -0,0 +1,101 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import itertools +from collections.abc import Iterable, Sequence + +import numpy as np +import pytest + +import cirq +from cirq.experiments.qubit_characterizations import _find_inv_matrix, _single_qubit_cliffords + + +def dot(args: Iterable[np.ndarray]) -> np.ndarray: + return functools.reduce(np.dot, args) + + +class TestSingleQubitRandomizedBenchmarking: + """Benchmarks circuit construction time for single qubit randomized benchmarking circuits. + + Given a combination of `depth`, `num_qubits` and `num_circuits`, the benchmark constructs + `num_circuits` different circuits, each spanning `num_qubits` and containing `depth` moments. + Each moment of the circuit contains a single qubit clifford operation for each qubit. + + Thus, the generated circuits have `depth * num_qubits` single qubit clifford operations. + """ + + group = "randomized_benchmarking" + depth = [1, 10, 50, 100, 250, 500, 1000] + num_qubits = [100] + num_circuits = [20] + + # assigned in setup_class + sq_xz_matrices: np.ndarray + sq_xz_cliffords: Sequence[cirq.Gate] + + @classmethod + def setup_class(cls): + cls.sq_xz_matrices = np.array( + [ + dot([cirq.unitary(c) for c in reversed(group)]) + for group in _single_qubit_cliffords().c1_in_xz + ] + ) + cls.sq_xz_cliffords = [cirq.PhasedXZGate.from_matrix(mat) for mat in cls.sq_xz_matrices] + + def _get_op_grid(self, qubits: Sequence[cirq.Qid], depth: int) -> list[list[cirq.Operation]]: + op_grid: list[list[cirq.Operation]] = [] + for q in qubits: + gate_ids = np.random.choice(len(self.sq_xz_cliffords), depth) + idx = _find_inv_matrix(dot(self.sq_xz_matrices[gate_ids][::-1]), self.sq_xz_matrices) + op_sequence = [self.sq_xz_cliffords[gate_id].on(q) for gate_id in gate_ids] + op_sequence.append(self.sq_xz_cliffords[idx].on(q)) + op_grid.append(op_sequence) + return op_grid + + @pytest.mark.parametrize( + ["depth", "num_qubits", "num_circuits"], itertools.product(depth, num_qubits, num_circuits) + ) + @pytest.mark.benchmark(group=group) + def test_rb_op_grid_generation( + self, benchmark, depth: int, num_qubits: int, num_circuits: int + ) -> None: + qubits = cirq.GridQubit.rect(1, num_qubits) + + def _f() -> None: + for _ in range(num_circuits): + self._get_op_grid(qubits, depth) + + benchmark(_f) + + @pytest.mark.parametrize( + ["depth", "num_qubits", "num_circuits"], itertools.product(depth, num_qubits, num_circuits) + ) + @pytest.mark.benchmark(group=group) + def test_rb_circuit_construction( + self, benchmark, depth: int, num_qubits: int, num_circuits: int + ) -> None: + qubits = cirq.GridQubit.rect(1, num_qubits) + + def _f() -> None: + for _ in range(num_circuits): + op_grid = self._get_op_grid(qubits, depth) + cirq.Circuit( + [cirq.Moment(ops[d] for ops in op_grid) for d in range(depth + 1)], + cirq.Moment(cirq.measure(*qubits)), + ) + + benchmark(_f) diff --git a/benchmarks/serialization_perf.py b/benchmarks/serialization_perf.py new file mode 100644 index 00000000000..5d52bee8f1f --- /dev/null +++ b/benchmarks/serialization_perf.py @@ -0,0 +1,75 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import cirq + +PARAMETERS = ( + # num_qubits num_moments expected_gzip_size + (100, 100, 31874), + (100, 1000, 316622), + (100, 4000, 1265955), + (500, 100, 147916), + (500, 1000, 1479772), + (500, 4000, 5918482), + (1000, 100, 285386), + (1000, 1000, 2853094), + (1000, 4000, 11412197), +) + + +def _make_circuit(num_qubits: int, num_moments: int) -> cirq.Circuit: + qubits = cirq.LineQubit.range(num_qubits) + one_q_x_moment = cirq.Moment(cirq.X(q) for q in qubits[::2]) + one_q_y_moment = cirq.Moment(cirq.Y(q) for q in qubits[1::2]) + two_q_cx_moment = cirq.Moment(cirq.CNOT(q1, q2) for q1, q2 in zip(qubits[::4], qubits[1::4])) + two_q_cz_moment = cirq.Moment(cirq.CZ(q1, q2) for q1, q2 in zip(qubits[::4], qubits[1::4])) + measurement_moment = cirq.Moment(cirq.measure_each(*qubits)) + circuit = cirq.Circuit( + [one_q_x_moment, two_q_cx_moment, one_q_y_moment, two_q_cz_moment, measurement_moment] + * (num_moments // 5) + ) + return circuit + + +@pytest.mark.parametrize( + ["num_qubits", "num_moments", "_expected_gzip_size"], + PARAMETERS, + ids=[f"{nq}-{nm}" for nq, nm, _ in PARAMETERS], +) +@pytest.mark.benchmark(group="serialization") +def test_json_serialization( + benchmark, num_qubits: int, num_moments: int, _expected_gzip_size: int +) -> None: + """Benchmark cirq.to_json.""" + circuit = _make_circuit(num_qubits, num_moments) + benchmark(cirq.to_json, circuit) + + +@pytest.mark.parametrize( + ["num_qubits", "num_moments", "expected_gzip_size"], + PARAMETERS, + ids=[f"{nq}-{nm}" for nq, nm, _ in PARAMETERS], +) +@pytest.mark.benchmark(group="serialization") +def test_json_gzip_serialization( + benchmark, num_qubits: int, num_moments: int, expected_gzip_size: int +) -> None: + """Benchmark cirq.to_json_gzip and check its output size.""" + circuit = _make_circuit(num_qubits, num_moments) + gzip_data = benchmark(cirq.to_json_gzip, circuit) + # tolerate absolute increase by 1KB or a relative increase by 1 per mille + allowed_size = expected_gzip_size + max(expected_gzip_size // 1000, 1024) + assert len(gzip_data) < allowed_size diff --git a/benchmarks/transformers/routing_perf.py b/benchmarks/transformers/routing_perf.py new file mode 100644 index 00000000000..fb02e3baa12 --- /dev/null +++ b/benchmarks/transformers/routing_perf.py @@ -0,0 +1,41 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Performance tests for circuit qubit routing.""" + +import itertools +from typing import Final + +import pytest + +import cirq + + +@pytest.mark.parametrize( + ["qubits", "depth"], itertools.product([10, 25, 50, 75, 100], [10, 50, 100, 250, 500, 1000]) +) +@pytest.mark.benchmark(group="circuit_routing", max_time=10) +def test_circuit_routing(benchmark, qubits: int, depth: int) -> None: + """Benchmark circuit construction for Rotated Bottom-Z Surface code.""" + op_density: Final = 0.5 + grid_device_size: Final = 10 + gate_domain: Final[dict[cirq.Gate, int]] = {cirq.CNOT: 2, cirq.X: 1} + + circuit = cirq.testing.random_circuit( + qubits, n_moments=depth, op_density=op_density, gate_domain=gate_domain, random_state=12345 + ) + device = cirq.testing.construct_grid_device(grid_device_size, grid_device_size) + router = cirq.RouteCQC(device.metadata.nx_graph) + routed_circuit = benchmark(router, circuit) + device.validate_circuit(routed_circuit) diff --git a/benchmarks/transformers/transformer_primitives_perf.py b/benchmarks/transformers/transformer_primitives_perf.py new file mode 100644 index 00000000000..937ea172150 --- /dev/null +++ b/benchmarks/transformers/transformer_primitives_perf.py @@ -0,0 +1,91 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import cirq + +PARAMETERS = ( + # num_qubits num_moments + (100, 100), + (100, 1000), + (100, 4000), + (500, 100), + (500, 1000), + (500, 4000), + (1000, 100), + (1000, 1000), + (1000, 4000), +) + + +def _make_circuit(num_qubits: int, num_moments: int) -> cirq.Circuit: + qubits = cirq.LineQubit.range(num_qubits) + one_q_x_moment = cirq.Moment(cirq.X(q) for q in qubits[::2]) + one_q_y_moment = cirq.Moment(cirq.Y(q) for q in qubits[1::2]) + two_q_cx_moment = cirq.Moment(cirq.CNOT(q1, q2) for q1, q2 in zip(qubits[::4], qubits[1::4])) + two_q_cz_moment = cirq.Moment(cirq.CZ(q1, q2) for q1, q2 in zip(qubits[::4], qubits[1::4])) + circuit = cirq.Circuit( + [one_q_x_moment, two_q_cx_moment, one_q_y_moment, two_q_cz_moment] * (num_moments // 4) + ) + return circuit + + +@pytest.mark.parametrize(["num_qubits", "num_moments"], PARAMETERS) +@pytest.mark.benchmark(group="transformer_primitives") +def test_map_moments(benchmark, num_qubits: int, num_moments: int) -> None: + circuit = _make_circuit(num_qubits, num_moments) + all_qubits = cirq.LineQubit.range(num_qubits) + + def map_func(m: cirq.Moment, _index: int) -> cirq.Moment: + new_ops = [op.with_tags("old op") for op in m.operations] + new_ops += [ + cirq.Z(q).with_tags("new op") for q in all_qubits if not m.operates_on_single_qubit(q) + ] + return cirq.Moment(new_ops) + + benchmark(cirq.map_moments, circuit=circuit, map_func=map_func) + + +@pytest.mark.parametrize(["num_qubits", "num_moments"], PARAMETERS) +@pytest.mark.benchmark(group="transformer_primitives") +def test_map_operations_apply_tag(benchmark, num_qubits: int, num_moments: int) -> None: + circuit = _make_circuit(num_qubits, num_moments) + + def map_func(op: cirq.Operation, _index: int) -> cirq.Operation: + return op.with_tags("old op") + + benchmark(cirq.map_operations, circuit=circuit, map_func=map_func) + + +@pytest.mark.parametrize(["num_qubits", "num_moments"], PARAMETERS) +@pytest.mark.benchmark(group="transformer_primitives") +def test_map_operations_to_optree(benchmark, num_qubits: int, num_moments: int) -> None: + circuit = _make_circuit(num_qubits, num_moments) + + def map_func(op: cirq.Operation, _index: int) -> cirq.OP_TREE: + return [op, op] + + benchmark(cirq.map_operations, circuit=circuit, map_func=map_func) + + +@pytest.mark.parametrize(["num_qubits", "num_moments"], PARAMETERS) +@pytest.mark.benchmark(group="transformer_primitives") +def test_map_operations_to_optree_and_unroll(benchmark, num_qubits: int, num_moments: int) -> None: + circuit = _make_circuit(num_qubits, num_moments) + + def map_func(op: cirq.Operation, _index: int) -> cirq.OP_TREE: + return [op, op] + + benchmark(cirq.map_operations_and_unroll, circuit=circuit, map_func=map_func)