diff --git a/benchmarks/circuit_construction_perf.py b/benchmarks/circuit_construction_perf.py new file mode 100644 index 00000000000..13498a8aaa5 --- /dev/null +++ b/benchmarks/circuit_construction_perf.py @@ -0,0 +1,164 @@ +# Copyright 2022 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.Qid], + z_measure_qubits: set[cirq.Qid], + x_measure_qubits: set[cirq.Qid], + 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"], [[1, 1], [10, 10], [100, 100], [1000, 1000]]) + @pytest.mark.benchmark(group=group) + def time_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..51d055683f7 --- /dev/null +++ b/benchmarks/linalg_decompositions_perf.py @@ -0,0 +1,26 @@ +# Copyright 2021 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..574d327756e --- /dev/null +++ b/benchmarks/parameter_resolution_perf.py @@ -0,0 +1,41 @@ +# Copyright 2022 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..3aee9cd39e0 --- /dev/null +++ b/benchmarks/randomized_benchmarking_perf.py @@ -0,0 +1,99 @@ +# Copyright 2022 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 Sequence + +import numpy as np +import pytest + +import cirq +from cirq.experiments.qubit_characterizations import _find_inv_matrix, _single_qubit_cliffords + + +def dot(args: Sequence[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] + + @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: list[cirq.Gate] = [ + cirq.PhasedXZGate.from_matrix(mat) for mat in cls.sq_xz_matrices + ] + + def _get_op_grid(self, qubits: list[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..e7bdbddb86a --- /dev/null +++ b/benchmarks/serialization_perf.py @@ -0,0 +1,75 @@ +# Copyright 2022 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