From 7283d796ef572fdc641ac6aef1603046649b6835 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 11 May 2026 16:32:25 -0700 Subject: [PATCH 1/9] Add `glue/stimflow` directory stimflow is a library for creating quantum error correction circuits --- .github/workflows/ci.yml | 19 + glue/stimflow/README.md | 129 + glue/stimflow/doc/api.md | 4391 ++++++++++ glue/stimflow/doc/getting_started.ipynb | 7656 +++++++++++++++++ glue/stimflow/requirements.txt | 2 + glue/stimflow/setup.py | 38 + glue/stimflow/src/stimflow/__init__.py | 60 + glue/stimflow/src/stimflow/_chunk/__init__.py | 20 + glue/stimflow/src/stimflow/_chunk/_chunk.py | 862 ++ .../src/stimflow/_chunk/_chunk_builder.py | 746 ++ .../stimflow/_chunk/_chunk_builder_test.py | 313 + .../src/stimflow/_chunk/_chunk_compiler.py | 656 ++ .../stimflow/_chunk/_chunk_compiler_test.py | 724 ++ .../src/stimflow/_chunk/_chunk_interface.py | 168 + .../src/stimflow/_chunk/_chunk_loop.py | 188 + .../src/stimflow/_chunk/_chunk_reflow.py | 309 + .../src/stimflow/_chunk/_chunk_reflow_test.py | 71 + .../src/stimflow/_chunk/_chunk_test.py | 551 ++ .../src/stimflow/_chunk/_code_util.py | 228 + .../src/stimflow/_chunk/_code_util_test.py | 165 + .../src/stimflow/_chunk/_flow_metadata.py | 29 + .../src/stimflow/_chunk/_flow_util.py | 273 + .../src/stimflow/_chunk/_flow_util_test.py | 164 + glue/stimflow/src/stimflow/_chunk/_patch.py | 182 + .../src/stimflow/_chunk/_patch_test.py | 49 + .../src/stimflow/_chunk/_stabilizer_code.py | 744 ++ .../stimflow/_chunk/_stabilizer_code_test.py | 314 + .../src/stimflow/_chunk/_test_util.py | 28 + glue/stimflow/src/stimflow/_chunk/_weave.py | 370 + .../src/stimflow/_chunk/_weave_test.py | 361 + glue/stimflow/src/stimflow/_core/__init__.py | 22 + .../src/stimflow/_core/_circuit_util.py | 415 + .../src/stimflow/_core/_circuit_util_test.py | 299 + .../src/stimflow/_core/_complex_util.py | 74 + .../src/stimflow/_core/_complex_util_test.py | 32 + glue/stimflow/src/stimflow/_core/_flow.py | 306 + .../stimflow/src/stimflow/_core/_flow_test.py | 7 + glue/stimflow/src/stimflow/_core/_noise.py | 662 ++ .../src/stimflow/_core/_noise_test.py | 368 + .../stimflow/src/stimflow/_core/_pauli_map.py | 264 + .../src/stimflow/_core/_pauli_map_test.py | 19 + glue/stimflow/src/stimflow/_core/_str_html.py | 41 + glue/stimflow/src/stimflow/_core/_str_svg.py | 41 + glue/stimflow/src/stimflow/_core/_tile.py | 183 + .../stimflow/src/stimflow/_core/_tile_test.py | 18 + .../stimflow/src/stimflow/_layers/__init__.py | 8 + glue/stimflow/src/stimflow/_layers/_data.py | 91 + .../src/stimflow/_layers/_data_test.py | 24 + .../_layers/_det_obs_annotation_layer.py | 42 + .../src/stimflow/_layers/_empty_layer.py | 25 + .../src/stimflow/_layers/_feedback_layer.py | 58 + .../stimflow/_layers/_feedback_layer_test.py | 7 + .../src/stimflow/_layers/_interact_layer.py | 92 + .../stimflow/_layers/_interact_swap_layer.py | 75 + .../_layers/_interact_swap_layer_test.py | 59 + .../src/stimflow/_layers/_iswap_layer.py | 34 + glue/stimflow/src/stimflow/_layers/_layer.py | 54 + .../src/stimflow/_layers/_layer_circuit.py | 808 ++ .../stimflow/_layers/_layer_circuit_test.py | 794 ++ .../src/stimflow/_layers/_loop_layer.py | 40 + .../src/stimflow/_layers/_measure_layer.py | 54 + .../src/stimflow/_layers/_mpp_layer.py | 30 + .../src/stimflow/_layers/_noise_layer.py | 33 + .../_layers/_qubit_coord_annotation_layer.py | 36 + .../src/stimflow/_layers/_reset_layer.py | 51 + .../src/stimflow/_layers/_rotation_layer.py | 89 + .../stimflow/_layers/_rotation_layer_test.py | 33 + .../_layers/_shift_coord_annotation_layer.py | 45 + .../src/stimflow/_layers/_sqrt_pp_layer.py | 57 + .../src/stimflow/_layers/_swap_layer.py | 81 + .../src/stimflow/_layers/_tag_layer.py | 37 + .../src/stimflow/_layers/_tag_layer_test.py | 58 + .../src/stimflow/_layers/_transpile.py | 32 + .../src/stimflow/_layers/_transpile_test.py | 635 ++ glue/stimflow/src/stimflow/_viz/_3d_model.py | 453 + .../src/stimflow/_viz/_3d_model_test.py | 121 + .../stimflow/_viz/_3d_model_text_texture.html | 57 + .../stimflow/_viz/_3d_model_text_texture.py | 2185 +++++ .../src/stimflow/_viz/_3d_model_viewer.py | 212 + glue/stimflow/src/stimflow/_viz/__init__.py | 12 + .../src/stimflow/_viz/_viz_circuit_html.py | 1094 +++ .../stimflow/_viz/_viz_circuit_html_test.py | 29 + .../stimflow/_viz/_viz_circuit_layer_svg.py | 199 + .../src/stimflow/_viz/_viz_patch_svg.py | 597 ++ .../src/stimflow/_viz/_viz_patch_svg_test.py | 34 + glue/stimflow/src/stimflow/_viz/_viz_svg.py | 175 + glue/stimflow/tools/gen_api_reference.py | 356 + 87 files changed, 31567 insertions(+) create mode 100644 glue/stimflow/README.md create mode 100644 glue/stimflow/doc/api.md create mode 100644 glue/stimflow/doc/getting_started.ipynb create mode 100644 glue/stimflow/requirements.txt create mode 100644 glue/stimflow/setup.py create mode 100644 glue/stimflow/src/stimflow/__init__.py create mode 100644 glue/stimflow/src/stimflow/_chunk/__init__.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_builder.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_interface.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_loop.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_chunk_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_code_util.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_code_util_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_flow_metadata.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_flow_util.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_flow_util_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_patch.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_patch_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_test_util.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_weave.py create mode 100644 glue/stimflow/src/stimflow/_chunk/_weave_test.py create mode 100644 glue/stimflow/src/stimflow/_core/__init__.py create mode 100644 glue/stimflow/src/stimflow/_core/_circuit_util.py create mode 100644 glue/stimflow/src/stimflow/_core/_circuit_util_test.py create mode 100644 glue/stimflow/src/stimflow/_core/_complex_util.py create mode 100644 glue/stimflow/src/stimflow/_core/_complex_util_test.py create mode 100644 glue/stimflow/src/stimflow/_core/_flow.py create mode 100644 glue/stimflow/src/stimflow/_core/_flow_test.py create mode 100644 glue/stimflow/src/stimflow/_core/_noise.py create mode 100644 glue/stimflow/src/stimflow/_core/_noise_test.py create mode 100644 glue/stimflow/src/stimflow/_core/_pauli_map.py create mode 100644 glue/stimflow/src/stimflow/_core/_pauli_map_test.py create mode 100644 glue/stimflow/src/stimflow/_core/_str_html.py create mode 100644 glue/stimflow/src/stimflow/_core/_str_svg.py create mode 100644 glue/stimflow/src/stimflow/_core/_tile.py create mode 100644 glue/stimflow/src/stimflow/_core/_tile_test.py create mode 100644 glue/stimflow/src/stimflow/_layers/__init__.py create mode 100644 glue/stimflow/src/stimflow/_layers/_data.py create mode 100644 glue/stimflow/src/stimflow/_layers/_data_test.py create mode 100644 glue/stimflow/src/stimflow/_layers/_det_obs_annotation_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_empty_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_feedback_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_feedback_layer_test.py create mode 100644 glue/stimflow/src/stimflow/_layers/_interact_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_interact_swap_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_interact_swap_layer_test.py create mode 100644 glue/stimflow/src/stimflow/_layers/_iswap_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_layer_circuit.py create mode 100644 glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py create mode 100644 glue/stimflow/src/stimflow/_layers/_loop_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_measure_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_mpp_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_noise_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_qubit_coord_annotation_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_reset_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_rotation_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_rotation_layer_test.py create mode 100644 glue/stimflow/src/stimflow/_layers/_shift_coord_annotation_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_sqrt_pp_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_swap_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_tag_layer.py create mode 100644 glue/stimflow/src/stimflow/_layers/_tag_layer_test.py create mode 100644 glue/stimflow/src/stimflow/_layers/_transpile.py create mode 100644 glue/stimflow/src/stimflow/_layers/_transpile_test.py create mode 100644 glue/stimflow/src/stimflow/_viz/_3d_model.py create mode 100644 glue/stimflow/src/stimflow/_viz/_3d_model_test.py create mode 100644 glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.html create mode 100644 glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.py create mode 100644 glue/stimflow/src/stimflow/_viz/_3d_model_viewer.py create mode 100644 glue/stimflow/src/stimflow/_viz/__init__.py create mode 100644 glue/stimflow/src/stimflow/_viz/_viz_circuit_html.py create mode 100644 glue/stimflow/src/stimflow/_viz/_viz_circuit_html_test.py create mode 100644 glue/stimflow/src/stimflow/_viz/_viz_circuit_layer_svg.py create mode 100644 glue/stimflow/src/stimflow/_viz/_viz_patch_svg.py create mode 100644 glue/stimflow/src/stimflow/_viz/_viz_patch_svg_test.py create mode 100644 glue/stimflow/src/stimflow/_viz/_viz_svg.py create mode 100755 glue/stimflow/tools/gen_api_reference.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 040221ad..46031c49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -420,6 +420,8 @@ jobs: - run: diff <(dev/compile_crumble_into_cpp_string_file.sh) src/stim/diagram/crumble_data.cc - run: pip install -e glue/sample - run: diff <(python dev/gen_sinter_api_reference.py -dev) doc/sinter_api.md + - run: pip install -e glue/stimflow + - run: diff <(python glue/stimflow/tools/gen_api_reference.py -dev) glue/stimflow/doc/api.md test_generated_file_lists_are_fresh: runs-on: ubuntu-24.04 steps: @@ -462,6 +464,23 @@ jobs: - run: pip install pytest - run: pytest glue/cirq - run: dev/doctest_proper.py --module stimcirq --import cirq sympy + test_stimflow: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + - uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3 + - uses: bazel-contrib/setup-bazel@e403ad507104847c3539436f64a9e9eecc73eeec # 0.8.5 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }} + repository-cache: true + bazelisk-version: 1.x + - run: bazel build :stim_dev_wheel + - run: pip install bazel-bin/stim-0.0.dev0-py3-none-any.whl + - run: pip install -e glue/stimflow + - run: pip install pytest + - run: pytest glue/stimflow + - run: dev/doctest_proper.py --module stimflow test_sinter: runs-on: ubuntu-24.04 steps: diff --git a/glue/stimflow/README.md b/glue/stimflow/README.md new file mode 100644 index 00000000..45bd273b --- /dev/null +++ b/glue/stimflow/README.md @@ -0,0 +1,129 @@ +stimflow: annealed utilities for creating QEC circuits +================================================= + +stimflow is a library for creating quantum error correction circuits. +In particular, stimflow decomposes the circuit creation problem into making and combining *chunks*. +A *chunk* is a circuit combined with stabilizer flow assertions the circuit is supposed to satisfy. +stimflow provides tools for making chunks, verifying chunks, debugging chunks, and compiling chunks into a complete final circuit. + +If you're getting started, see stimflow's [getting started notebook](doc/getting_started.ipynb). + +See stimflow's [API reference](doc/api.md) for the suite available methods and types. + +stimflow's design philosophy is to be a tool box, not a black box. +For example, stimflow does not include methods for creating surface code circuits or other standard codes. +Instead it provides tools that can be used to more easily create those circuits. + +# Backwards Compatibility Warning + +Stimflow does not currently guarantee backwards compatibility. +There are parts of the library that do not yet feel like they have converged on the "right" way to do it, +and I want to maintain the freedom to fix them later. + +# Example Usage: Surface Code Circuit + +stimflow is not yet provided as a pypi package, so you cannot install it with pip. +The installation can be done manually by copying the contents of this directory somewhere into your python path. + +The following is python code that emits a surface code circuit. + +```python +import stimflow as sf + + +def make_surface_code(d: int) -> sf.StabilizerCode: + """Defines the stabilizers and observables of a surface code.""" + tiles = [] + ds = [0, 1, 1j, 1 + 1j] + for x in range(-1, d): + for y in range(-1, d): + m = x + 1j * y + qs = [m + d for d in ds] + qs = [q for q in qs + if 0 <= q.real < d and 0 <= q.imag < d] + b = 'XZ'[(x + y) % 2] + if b == 'X' and x in [-1, d - 1]: + continue + if b == 'Z' and y in [-1, d - 1]: + continue + tiles.append(sf.Tile( + data_qubits=qs, + bases=b, + measure_qubit=m + 0.5 + 0.5j, + )) + + patch = sf.Patch(tiles) + obs_x = sf.PauliMap.from_xs([q for q in patch.data_set if q.real == 0]).with_name('X') + obs_z = sf.PauliMap.from_zs([q for q in patch.data_set if q.imag == 0]).with_name('Z') + return sf.StabilizerCode(patch, logicals=[(obs_x, obs_z)]) + + +def make_idle_round(d: int) -> sf.Chunk: + """Creates a circuit that performs one round of surface code stabilizer measurement.""" + code = make_surface_code(d=d) + builder = sf.ChunkBuilder(allowed_qubits=code.used_set) + mxs = [tile.measure_qubit for tile in code.tiles if tile.basis == 'X'] + mzs = [tile.measure_qubit for tile in code.tiles if tile.basis == 'Z'] + + # Prepare measure qubits. + builder.append("RX", mxs) + builder.append("RZ", mzs) + builder.append("TICK") + + # Perform entangling gates. + dxs = [-0.5 - 0.5j, 0.5 - 0.5j, -0.5 + 0.5j, 0.5 + 0.5j] + dzs = [dxs[0], dxs[2], dxs[1], dxs[3]] + for k in range(4): + builder.append( + 'CX', + [(m, m + dxs[k]) for m in mxs] + [(m + dzs[k], m) for m in mzs], + unknown_qubit_append_mode='skip', + ) + builder.append("TICK") + + # Measure the measure qubits. + builder.append("MX", mxs) + builder.append("MZ", mzs) + + # Assert the circuit should be preparing and measuring the stabilizers. + for tile in code.tiles: + builder.add_flow(start=tile, ms=[tile.measure_qubit]) + builder.add_flow(end=tile, ms=[tile.measure_qubit]) + # Assert the circuit should be preserving the logical operators. + for obs in code.flat_logicals: + builder.add_flow(start=obs, end=obs) + + return builder.finish_chunk() + + +def main(): + # Create the code, verify its commutation relationships, and save a picture of it. + code = make_surface_code(d=7) + code.verify() + code.to_svg().write_to('tmp.svg') + + # Create the circuit cycle, verify its operation, and create an interactive viewer. + chunk = make_idle_round(d=7) + chunk.to_html_viewer(background=code).write_to('tmp.html') + chunk.verify() + + # Compile a physical memory experiment with alternating cycle orderings. + compiler = sf.ChunkCompiler() + compiler.append(code.transversal_init_chunk(basis='X')) + compiler.append(sf.ChunkLoop( + [chunk, chunk.time_reversed()], + repetitions=5, + )) + compiler.append(code.transversal_measure_chunk(basis='X')) + circuit = compiler.finish_circuit() + + # Add noise to the circuit, check its distance, and make another viewer. + noisy_circuit = sf.NoiseModel.uniform_depolarizing(1e-3).noisy_circuit(circuit) + distance = len(noisy_circuit.shortest_graphlike_error()) + assert distance == 7 + sf.stim_circuit_html_viewer(noisy_circuit, background=code).write_to('tmp2.html') + + +if __name__ == "__main__": + main() +``` \ No newline at end of file diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md new file mode 100644 index 00000000..2f64dcd4 --- /dev/null +++ b/glue/stimflow/doc/api.md @@ -0,0 +1,4391 @@ +# stimflow v0.1.0 API Reference + +## Index +- [`stimflow.Chunk`](#stimflow.Chunk) + - [`stimflow.Chunk.__init__`](#stimflow.Chunk.__init__) + - [`stimflow.Chunk.end_code`](#stimflow.Chunk.end_code) + - [`stimflow.Chunk.end_interface`](#stimflow.Chunk.end_interface) + - [`stimflow.Chunk.end_patch`](#stimflow.Chunk.end_patch) + - [`stimflow.Chunk.find_distance`](#stimflow.Chunk.find_distance) + - [`stimflow.Chunk.find_logical_error`](#stimflow.Chunk.find_logical_error) + - [`stimflow.Chunk.flattened`](#stimflow.Chunk.flattened) + - [`stimflow.Chunk.from_circuit_with_mpp_boundaries`](#stimflow.Chunk.from_circuit_with_mpp_boundaries) + - [`stimflow.Chunk.start_code`](#stimflow.Chunk.start_code) + - [`stimflow.Chunk.start_interface`](#stimflow.Chunk.start_interface) + - [`stimflow.Chunk.start_patch`](#stimflow.Chunk.start_patch) + - [`stimflow.Chunk.then`](#stimflow.Chunk.then) + - [`stimflow.Chunk.time_reversed`](#stimflow.Chunk.time_reversed) + - [`stimflow.Chunk.to_closed_circuit`](#stimflow.Chunk.to_closed_circuit) + - [`stimflow.Chunk.to_coord_circuit`](#stimflow.Chunk.to_coord_circuit) + - [`stimflow.Chunk.to_html_viewer`](#stimflow.Chunk.to_html_viewer) + - [`stimflow.Chunk.verify`](#stimflow.Chunk.verify) + - [`stimflow.Chunk.verify_distance_is_at_least_2`](#stimflow.Chunk.verify_distance_is_at_least_2) + - [`stimflow.Chunk.verify_distance_is_at_least_3`](#stimflow.Chunk.verify_distance_is_at_least_3) + - [`stimflow.Chunk.with_edits`](#stimflow.Chunk.with_edits) + - [`stimflow.Chunk.with_flag_added_to_all_flows`](#stimflow.Chunk.with_flag_added_to_all_flows) + - [`stimflow.Chunk.with_obs_flows_as_det_flows`](#stimflow.Chunk.with_obs_flows_as_det_flows) + - [`stimflow.Chunk.with_repetitions`](#stimflow.Chunk.with_repetitions) + - [`stimflow.Chunk.with_transformed_coords`](#stimflow.Chunk.with_transformed_coords) + - [`stimflow.Chunk.with_xz_flipped`](#stimflow.Chunk.with_xz_flipped) +- [`stimflow.ChunkBuilder`](#stimflow.ChunkBuilder) + - [`stimflow.ChunkBuilder.__init__`](#stimflow.ChunkBuilder.__init__) + - [`stimflow.ChunkBuilder.add_discarded_flow_input`](#stimflow.ChunkBuilder.add_discarded_flow_input) + - [`stimflow.ChunkBuilder.add_discarded_flow_output`](#stimflow.ChunkBuilder.add_discarded_flow_output) + - [`stimflow.ChunkBuilder.add_flow`](#stimflow.ChunkBuilder.add_flow) + - [`stimflow.ChunkBuilder.append`](#stimflow.ChunkBuilder.append) + - [`stimflow.ChunkBuilder.append_feedback`](#stimflow.ChunkBuilder.append_feedback) + - [`stimflow.ChunkBuilder.finish_chunk`](#stimflow.ChunkBuilder.finish_chunk) + - [`stimflow.ChunkBuilder.has_measurement`](#stimflow.ChunkBuilder.has_measurement) + - [`stimflow.ChunkBuilder.lookup_mids`](#stimflow.ChunkBuilder.lookup_mids) + - [`stimflow.ChunkBuilder.record_measurement_group`](#stimflow.ChunkBuilder.record_measurement_group) +- [`stimflow.ChunkCompiler`](#stimflow.ChunkCompiler) + - [`stimflow.ChunkCompiler.__init__`](#stimflow.ChunkCompiler.__init__) + - [`stimflow.ChunkCompiler.append`](#stimflow.ChunkCompiler.append) + - [`stimflow.ChunkCompiler.append_magic_end_chunk`](#stimflow.ChunkCompiler.append_magic_end_chunk) + - [`stimflow.ChunkCompiler.append_magic_init_chunk`](#stimflow.ChunkCompiler.append_magic_init_chunk) + - [`stimflow.ChunkCompiler.copy`](#stimflow.ChunkCompiler.copy) + - [`stimflow.ChunkCompiler.cur_circuit_html_viewer`](#stimflow.ChunkCompiler.cur_circuit_html_viewer) + - [`stimflow.ChunkCompiler.cur_end_interface`](#stimflow.ChunkCompiler.cur_end_interface) + - [`stimflow.ChunkCompiler.ensure_observables_included`](#stimflow.ChunkCompiler.ensure_observables_included) + - [`stimflow.ChunkCompiler.ensure_qubits_included`](#stimflow.ChunkCompiler.ensure_qubits_included) + - [`stimflow.ChunkCompiler.finish_circuit`](#stimflow.ChunkCompiler.finish_circuit) +- [`stimflow.ChunkInterface`](#stimflow.ChunkInterface) + - [`stimflow.ChunkInterface.data_set`](#stimflow.ChunkInterface.data_set) + - [`stimflow.ChunkInterface.partitioned_detector_flows`](#stimflow.ChunkInterface.partitioned_detector_flows) + - [`stimflow.ChunkInterface.to_code`](#stimflow.ChunkInterface.to_code) + - [`stimflow.ChunkInterface.to_patch`](#stimflow.ChunkInterface.to_patch) + - [`stimflow.ChunkInterface.to_svg`](#stimflow.ChunkInterface.to_svg) + - [`stimflow.ChunkInterface.used_set`](#stimflow.ChunkInterface.used_set) + - [`stimflow.ChunkInterface.with_discards_as_ports`](#stimflow.ChunkInterface.with_discards_as_ports) + - [`stimflow.ChunkInterface.with_edits`](#stimflow.ChunkInterface.with_edits) + - [`stimflow.ChunkInterface.with_transformed_coords`](#stimflow.ChunkInterface.with_transformed_coords) + - [`stimflow.ChunkInterface.without_discards`](#stimflow.ChunkInterface.without_discards) + - [`stimflow.ChunkInterface.without_keyed`](#stimflow.ChunkInterface.without_keyed) +- [`stimflow.ChunkLoop`](#stimflow.ChunkLoop) + - [`stimflow.ChunkLoop.end_interface`](#stimflow.ChunkLoop.end_interface) + - [`stimflow.ChunkLoop.end_patch`](#stimflow.ChunkLoop.end_patch) + - [`stimflow.ChunkLoop.find_distance`](#stimflow.ChunkLoop.find_distance) + - [`stimflow.ChunkLoop.find_logical_error`](#stimflow.ChunkLoop.find_logical_error) + - [`stimflow.ChunkLoop.flattened`](#stimflow.ChunkLoop.flattened) + - [`stimflow.ChunkLoop.start_interface`](#stimflow.ChunkLoop.start_interface) + - [`stimflow.ChunkLoop.start_patch`](#stimflow.ChunkLoop.start_patch) + - [`stimflow.ChunkLoop.time_reversed`](#stimflow.ChunkLoop.time_reversed) + - [`stimflow.ChunkLoop.to_closed_circuit`](#stimflow.ChunkLoop.to_closed_circuit) + - [`stimflow.ChunkLoop.to_html_viewer`](#stimflow.ChunkLoop.to_html_viewer) + - [`stimflow.ChunkLoop.verify`](#stimflow.ChunkLoop.verify) + - [`stimflow.ChunkLoop.verify_distance_is_at_least_2`](#stimflow.ChunkLoop.verify_distance_is_at_least_2) + - [`stimflow.ChunkLoop.verify_distance_is_at_least_3`](#stimflow.ChunkLoop.verify_distance_is_at_least_3) + - [`stimflow.ChunkLoop.with_repetitions`](#stimflow.ChunkLoop.with_repetitions) +- [`stimflow.ChunkReflow`](#stimflow.ChunkReflow) + - [`stimflow.ChunkReflow.end_code`](#stimflow.ChunkReflow.end_code) + - [`stimflow.ChunkReflow.end_interface`](#stimflow.ChunkReflow.end_interface) + - [`stimflow.ChunkReflow.end_patch`](#stimflow.ChunkReflow.end_patch) + - [`stimflow.ChunkReflow.flattened`](#stimflow.ChunkReflow.flattened) + - [`stimflow.ChunkReflow.from_auto_rewrite`](#stimflow.ChunkReflow.from_auto_rewrite) + - [`stimflow.ChunkReflow.from_auto_rewrite_transitions_using_stable`](#stimflow.ChunkReflow.from_auto_rewrite_transitions_using_stable) + - [`stimflow.ChunkReflow.removed_inputs`](#stimflow.ChunkReflow.removed_inputs) + - [`stimflow.ChunkReflow.start_code`](#stimflow.ChunkReflow.start_code) + - [`stimflow.ChunkReflow.start_interface`](#stimflow.ChunkReflow.start_interface) + - [`stimflow.ChunkReflow.start_patch`](#stimflow.ChunkReflow.start_patch) + - [`stimflow.ChunkReflow.verify`](#stimflow.ChunkReflow.verify) + - [`stimflow.ChunkReflow.with_obs_flows_as_det_flows`](#stimflow.ChunkReflow.with_obs_flows_as_det_flows) + - [`stimflow.ChunkReflow.with_transformed_coords`](#stimflow.ChunkReflow.with_transformed_coords) +- [`stimflow.Flow`](#stimflow.Flow) + - [`stimflow.Flow.__init__`](#stimflow.Flow.__init__) + - [`stimflow.Flow.__mul__`](#stimflow.Flow.__mul__) + - [`stimflow.Flow.fuse_with_next_flow`](#stimflow.Flow.fuse_with_next_flow) + - [`stimflow.Flow.obs_key`](#stimflow.Flow.obs_key) + - [`stimflow.Flow.to_stim_flow`](#stimflow.Flow.to_stim_flow) + - [`stimflow.Flow.with_edits`](#stimflow.Flow.with_edits) + - [`stimflow.Flow.with_transformed_coords`](#stimflow.Flow.with_transformed_coords) + - [`stimflow.Flow.with_xz_flipped`](#stimflow.Flow.with_xz_flipped) +- [`stimflow.FlowMetadata`](#stimflow.FlowMetadata) + - [`stimflow.FlowMetadata.__init__`](#stimflow.FlowMetadata.__init__) +- [`stimflow.InteractLayer`](#stimflow.InteractLayer) + - [`stimflow.InteractLayer.append_into_stim_circuit`](#stimflow.InteractLayer.append_into_stim_circuit) + - [`stimflow.InteractLayer.copy`](#stimflow.InteractLayer.copy) + - [`stimflow.InteractLayer.locally_optimized`](#stimflow.InteractLayer.locally_optimized) + - [`stimflow.InteractLayer.rotate_to_z_layer`](#stimflow.InteractLayer.rotate_to_z_layer) + - [`stimflow.InteractLayer.to_z_basis`](#stimflow.InteractLayer.to_z_basis) + - [`stimflow.InteractLayer.touched`](#stimflow.InteractLayer.touched) +- [`stimflow.LayerCircuit`](#stimflow.LayerCircuit) + - [`stimflow.LayerCircuit.copy`](#stimflow.LayerCircuit.copy) + - [`stimflow.LayerCircuit.from_stim_circuit`](#stimflow.LayerCircuit.from_stim_circuit) + - [`stimflow.LayerCircuit.to_stim_circuit`](#stimflow.LayerCircuit.to_stim_circuit) + - [`stimflow.LayerCircuit.to_z_basis`](#stimflow.LayerCircuit.to_z_basis) + - [`stimflow.LayerCircuit.touched`](#stimflow.LayerCircuit.touched) + - [`stimflow.LayerCircuit.with_cleaned_up_loop_iterations`](#stimflow.LayerCircuit.with_cleaned_up_loop_iterations) + - [`stimflow.LayerCircuit.with_clearable_rotation_layers_cleared`](#stimflow.LayerCircuit.with_clearable_rotation_layers_cleared) + - [`stimflow.LayerCircuit.with_ejected_loop_iterations`](#stimflow.LayerCircuit.with_ejected_loop_iterations) + - [`stimflow.LayerCircuit.with_irrelevant_tail_layers_removed`](#stimflow.LayerCircuit.with_irrelevant_tail_layers_removed) + - [`stimflow.LayerCircuit.with_locally_merged_measure_layers`](#stimflow.LayerCircuit.with_locally_merged_measure_layers) + - [`stimflow.LayerCircuit.with_locally_optimized_layers`](#stimflow.LayerCircuit.with_locally_optimized_layers) + - [`stimflow.LayerCircuit.with_qubit_coords_at_start`](#stimflow.LayerCircuit.with_qubit_coords_at_start) + - [`stimflow.LayerCircuit.with_rotations_before_resets_removed`](#stimflow.LayerCircuit.with_rotations_before_resets_removed) + - [`stimflow.LayerCircuit.with_rotations_merged_earlier`](#stimflow.LayerCircuit.with_rotations_merged_earlier) + - [`stimflow.LayerCircuit.with_rotations_rolled_from_end_of_loop_to_start_of_loop`](#stimflow.LayerCircuit.with_rotations_rolled_from_end_of_loop_to_start_of_loop) + - [`stimflow.LayerCircuit.with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer`](#stimflow.LayerCircuit.with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer) + - [`stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type`](#stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type) + - [`stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier`](#stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier) + - [`stimflow.LayerCircuit.without_empty_layers`](#stimflow.LayerCircuit.without_empty_layers) +- [`stimflow.LineData`](#stimflow.LineData) + - [`stimflow.LineData.__init__`](#stimflow.LineData.__init__) + - [`stimflow.LineData.fused`](#stimflow.LineData.fused) +- [`stimflow.MeasureLayer`](#stimflow.MeasureLayer) + - [`stimflow.MeasureLayer.append_into_stim_circuit`](#stimflow.MeasureLayer.append_into_stim_circuit) + - [`stimflow.MeasureLayer.copy`](#stimflow.MeasureLayer.copy) + - [`stimflow.MeasureLayer.locally_optimized`](#stimflow.MeasureLayer.locally_optimized) + - [`stimflow.MeasureLayer.to_z_basis`](#stimflow.MeasureLayer.to_z_basis) + - [`stimflow.MeasureLayer.touched`](#stimflow.MeasureLayer.touched) +- [`stimflow.NoiseModel`](#stimflow.NoiseModel) + - [`stimflow.NoiseModel.noisy_circuit`](#stimflow.NoiseModel.noisy_circuit) + - [`stimflow.NoiseModel.noisy_circuit_skipping_mpp_boundaries`](#stimflow.NoiseModel.noisy_circuit_skipping_mpp_boundaries) + - [`stimflow.NoiseModel.si1000`](#stimflow.NoiseModel.si1000) + - [`stimflow.NoiseModel.uniform_depolarizing`](#stimflow.NoiseModel.uniform_depolarizing) +- [`stimflow.NoiseRule`](#stimflow.NoiseRule) + - [`stimflow.NoiseRule.__init__`](#stimflow.NoiseRule.__init__) + - [`stimflow.NoiseRule.append_noisy_version_of`](#stimflow.NoiseRule.append_noisy_version_of) +- [`stimflow.Patch`](#stimflow.Patch) + - [`stimflow.Patch.data_set`](#stimflow.Patch.data_set) + - [`stimflow.Patch.m2tile`](#stimflow.Patch.m2tile) + - [`stimflow.Patch.measure_set`](#stimflow.Patch.measure_set) + - [`stimflow.Patch.partitioned_tiles`](#stimflow.Patch.partitioned_tiles) + - [`stimflow.Patch.to_svg`](#stimflow.Patch.to_svg) + - [`stimflow.Patch.used_set`](#stimflow.Patch.used_set) + - [`stimflow.Patch.with_edits`](#stimflow.Patch.with_edits) + - [`stimflow.Patch.with_only_x_tiles`](#stimflow.Patch.with_only_x_tiles) + - [`stimflow.Patch.with_only_y_tiles`](#stimflow.Patch.with_only_y_tiles) + - [`stimflow.Patch.with_only_z_tiles`](#stimflow.Patch.with_only_z_tiles) + - [`stimflow.Patch.with_remaining_degrees_of_freedom_as_logicals`](#stimflow.Patch.with_remaining_degrees_of_freedom_as_logicals) + - [`stimflow.Patch.with_transformed_bases`](#stimflow.Patch.with_transformed_bases) + - [`stimflow.Patch.with_transformed_coords`](#stimflow.Patch.with_transformed_coords) + - [`stimflow.Patch.with_xz_flipped`](#stimflow.Patch.with_xz_flipped) +- [`stimflow.PauliMap`](#stimflow.PauliMap) + - [`stimflow.PauliMap.__init__`](#stimflow.PauliMap.__init__) + - [`stimflow.PauliMap.anticommutes`](#stimflow.PauliMap.anticommutes) + - [`stimflow.PauliMap.commutes`](#stimflow.PauliMap.commutes) + - [`stimflow.PauliMap.from_xs`](#stimflow.PauliMap.from_xs) + - [`stimflow.PauliMap.from_ys`](#stimflow.PauliMap.from_ys) + - [`stimflow.PauliMap.from_zs`](#stimflow.PauliMap.from_zs) + - [`stimflow.PauliMap.get`](#stimflow.PauliMap.get) + - [`stimflow.PauliMap.items`](#stimflow.PauliMap.items) + - [`stimflow.PauliMap.keys`](#stimflow.PauliMap.keys) + - [`stimflow.PauliMap.to_stim_pauli_string`](#stimflow.PauliMap.to_stim_pauli_string) + - [`stimflow.PauliMap.to_stim_targets`](#stimflow.PauliMap.to_stim_targets) + - [`stimflow.PauliMap.to_tile`](#stimflow.PauliMap.to_tile) + - [`stimflow.PauliMap.values`](#stimflow.PauliMap.values) + - [`stimflow.PauliMap.with_basis`](#stimflow.PauliMap.with_basis) + - [`stimflow.PauliMap.with_name`](#stimflow.PauliMap.with_name) + - [`stimflow.PauliMap.with_transformed_coords`](#stimflow.PauliMap.with_transformed_coords) + - [`stimflow.PauliMap.with_xy_flipped`](#stimflow.PauliMap.with_xy_flipped) + - [`stimflow.PauliMap.with_xz_flipped`](#stimflow.PauliMap.with_xz_flipped) +- [`stimflow.ResetLayer`](#stimflow.ResetLayer) + - [`stimflow.ResetLayer.append_into_stim_circuit`](#stimflow.ResetLayer.append_into_stim_circuit) + - [`stimflow.ResetLayer.copy`](#stimflow.ResetLayer.copy) + - [`stimflow.ResetLayer.locally_optimized`](#stimflow.ResetLayer.locally_optimized) + - [`stimflow.ResetLayer.to_z_basis`](#stimflow.ResetLayer.to_z_basis) + - [`stimflow.ResetLayer.touched`](#stimflow.ResetLayer.touched) +- [`stimflow.RotationLayer`](#stimflow.RotationLayer) + - [`stimflow.RotationLayer.append_into_stim_circuit`](#stimflow.RotationLayer.append_into_stim_circuit) + - [`stimflow.RotationLayer.append_named_rotation`](#stimflow.RotationLayer.append_named_rotation) + - [`stimflow.RotationLayer.copy`](#stimflow.RotationLayer.copy) + - [`stimflow.RotationLayer.inverse`](#stimflow.RotationLayer.inverse) + - [`stimflow.RotationLayer.is_vacuous`](#stimflow.RotationLayer.is_vacuous) + - [`stimflow.RotationLayer.locally_optimized`](#stimflow.RotationLayer.locally_optimized) + - [`stimflow.RotationLayer.prepend_named_rotation`](#stimflow.RotationLayer.prepend_named_rotation) + - [`stimflow.RotationLayer.touched`](#stimflow.RotationLayer.touched) +- [`stimflow.StabilizerCode`](#stimflow.StabilizerCode) + - [`stimflow.StabilizerCode.__init__`](#stimflow.StabilizerCode.__init__) + - [`stimflow.StabilizerCode.as_interface`](#stimflow.StabilizerCode.as_interface) + - [`stimflow.StabilizerCode.concat_over`](#stimflow.StabilizerCode.concat_over) + - [`stimflow.StabilizerCode.data_set`](#stimflow.StabilizerCode.data_set) + - [`stimflow.StabilizerCode.find_distance`](#stimflow.StabilizerCode.find_distance) + - [`stimflow.StabilizerCode.find_logical_error`](#stimflow.StabilizerCode.find_logical_error) + - [`stimflow.StabilizerCode.flat_logicals`](#stimflow.StabilizerCode.flat_logicals) + - [`stimflow.StabilizerCode.from_patch_with_inferred_observables`](#stimflow.StabilizerCode.from_patch_with_inferred_observables) + - [`stimflow.StabilizerCode.get_observable_by_basis`](#stimflow.StabilizerCode.get_observable_by_basis) + - [`stimflow.StabilizerCode.list_pure_basis_observables`](#stimflow.StabilizerCode.list_pure_basis_observables) + - [`stimflow.StabilizerCode.make_code_capacity_circuit`](#stimflow.StabilizerCode.make_code_capacity_circuit) + - [`stimflow.StabilizerCode.make_phenom_circuit`](#stimflow.StabilizerCode.make_phenom_circuit) + - [`stimflow.StabilizerCode.measure_set`](#stimflow.StabilizerCode.measure_set) + - [`stimflow.StabilizerCode.patch`](#stimflow.StabilizerCode.patch) + - [`stimflow.StabilizerCode.physical_to_logical`](#stimflow.StabilizerCode.physical_to_logical) + - [`stimflow.StabilizerCode.tiles`](#stimflow.StabilizerCode.tiles) + - [`stimflow.StabilizerCode.to_svg`](#stimflow.StabilizerCode.to_svg) + - [`stimflow.StabilizerCode.transversal_init_chunk`](#stimflow.StabilizerCode.transversal_init_chunk) + - [`stimflow.StabilizerCode.transversal_measure_chunk`](#stimflow.StabilizerCode.transversal_measure_chunk) + - [`stimflow.StabilizerCode.used_set`](#stimflow.StabilizerCode.used_set) + - [`stimflow.StabilizerCode.verify`](#stimflow.StabilizerCode.verify) + - [`stimflow.StabilizerCode.verify_distance_is_at_least_2`](#stimflow.StabilizerCode.verify_distance_is_at_least_2) + - [`stimflow.StabilizerCode.verify_distance_is_at_least_3`](#stimflow.StabilizerCode.verify_distance_is_at_least_3) + - [`stimflow.StabilizerCode.with_edits`](#stimflow.StabilizerCode.with_edits) + - [`stimflow.StabilizerCode.with_integer_coordinates`](#stimflow.StabilizerCode.with_integer_coordinates) + - [`stimflow.StabilizerCode.with_observables_from_basis`](#stimflow.StabilizerCode.with_observables_from_basis) + - [`stimflow.StabilizerCode.with_remaining_degrees_of_freedom_as_logicals`](#stimflow.StabilizerCode.with_remaining_degrees_of_freedom_as_logicals) + - [`stimflow.StabilizerCode.with_transformed_coords`](#stimflow.StabilizerCode.with_transformed_coords) + - [`stimflow.StabilizerCode.with_xz_flipped`](#stimflow.StabilizerCode.with_xz_flipped) + - [`stimflow.StabilizerCode.x_basis_subset`](#stimflow.StabilizerCode.x_basis_subset) + - [`stimflow.StabilizerCode.z_basis_subset`](#stimflow.StabilizerCode.z_basis_subset) +- [`stimflow.StimCircuitLoom`](#stimflow.StimCircuitLoom) + - [`stimflow.StimCircuitLoom.weave`](#stimflow.StimCircuitLoom.weave) + - [`stimflow.StimCircuitLoom.weaved_target_rec_from_c0`](#stimflow.StimCircuitLoom.weaved_target_rec_from_c0) + - [`stimflow.StimCircuitLoom.weaved_target_rec_from_c1`](#stimflow.StimCircuitLoom.weaved_target_rec_from_c1) +- [`stimflow.TextData`](#stimflow.TextData) + - [`stimflow.TextData.__init__`](#stimflow.TextData.__init__) +- [`stimflow.Tile`](#stimflow.Tile) + - [`stimflow.Tile.__init__`](#stimflow.Tile.__init__) + - [`stimflow.Tile.basis`](#stimflow.Tile.basis) + - [`stimflow.Tile.center`](#stimflow.Tile.center) + - [`stimflow.Tile.data_set`](#stimflow.Tile.data_set) + - [`stimflow.Tile.to_pauli_map`](#stimflow.Tile.to_pauli_map) + - [`stimflow.Tile.used_set`](#stimflow.Tile.used_set) + - [`stimflow.Tile.with_bases`](#stimflow.Tile.with_bases) + - [`stimflow.Tile.with_basis`](#stimflow.Tile.with_basis) + - [`stimflow.Tile.with_data_qubit_cleared`](#stimflow.Tile.with_data_qubit_cleared) + - [`stimflow.Tile.with_edits`](#stimflow.Tile.with_edits) + - [`stimflow.Tile.with_transformed_bases`](#stimflow.Tile.with_transformed_bases) + - [`stimflow.Tile.with_transformed_coords`](#stimflow.Tile.with_transformed_coords) + - [`stimflow.Tile.with_xz_flipped`](#stimflow.Tile.with_xz_flipped) +- [`stimflow.TriangleData`](#stimflow.TriangleData) + - [`stimflow.TriangleData.__init__`](#stimflow.TriangleData.__init__) + - [`stimflow.TriangleData.fused`](#stimflow.TriangleData.fused) + - [`stimflow.TriangleData.rect`](#stimflow.TriangleData.rect) +- [`stimflow.Viewable3dModelGLTF`](#stimflow.Viewable3dModelGLTF) + - [`stimflow.Viewable3dModelGLTF.html_viewer`](#stimflow.Viewable3dModelGLTF.html_viewer) +- [`stimflow.append_reindexed_content_to_circuit`](#stimflow.append_reindexed_content_to_circuit) +- [`stimflow.circuit_to_cycle_code_slices`](#stimflow.circuit_to_cycle_code_slices) +- [`stimflow.circuit_to_dem_target_measurement_records_map`](#stimflow.circuit_to_dem_target_measurement_records_map) +- [`stimflow.circuit_with_xz_flipped`](#stimflow.circuit_with_xz_flipped) +- [`stimflow.compile_chunks_into_circuit`](#stimflow.compile_chunks_into_circuit) +- [`stimflow.complex_key`](#stimflow.complex_key) +- [`stimflow.count_measurement_layers`](#stimflow.count_measurement_layers) +- [`stimflow.find_d1_error`](#stimflow.find_d1_error) +- [`stimflow.find_d2_error`](#stimflow.find_d2_error) +- [`stimflow.gate_counts_for_circuit`](#stimflow.gate_counts_for_circuit) +- [`stimflow.gates_used_by_circuit`](#stimflow.gates_used_by_circuit) +- [`stimflow.html_viewer_for_gltf_model`](#stimflow.html_viewer_for_gltf_model) +- [`stimflow.make_3d_model`](#stimflow.make_3d_model) +- [`stimflow.min_max_complex`](#stimflow.min_max_complex) +- [`stimflow.sorted_complex`](#stimflow.sorted_complex) +- [`stimflow.stim_circuit_html_viewer`](#stimflow.stim_circuit_html_viewer) +- [`stimflow.stim_circuit_with_transformed_coords`](#stimflow.stim_circuit_with_transformed_coords) +- [`stimflow.stim_circuit_with_transformed_moments`](#stimflow.stim_circuit_with_transformed_moments) +- [`stimflow.str_html`](#stimflow.str_html) + - [`stimflow.str_html.write_to`](#stimflow.str_html.write_to) +- [`stimflow.str_svg`](#stimflow.str_svg) + - [`stimflow.str_svg.write_to`](#stimflow.str_svg.write_to) +- [`stimflow.svg`](#stimflow.svg) +- [`stimflow.transpile_to_z_basis_interaction_circuit`](#stimflow.transpile_to_z_basis_interaction_circuit) +- [`stimflow.transversal_code_transition_chunks`](#stimflow.transversal_code_transition_chunks) +- [`stimflow.verify_distance_is_at_least_2`](#stimflow.verify_distance_is_at_least_2) +- [`stimflow.verify_distance_is_at_least_3`](#stimflow.verify_distance_is_at_least_3) +- [`stimflow.xor_sorted`](#stimflow.xor_sorted) +```python +# Types used by the method definitions. +from __future__ import annotations +from typing import overload, TYPE_CHECKING, Any, Iterable +import io +import pathlib +import numpy as np +``` + + +```python +# stimflow.Chunk + +# (at top-level in the stimflow module) +class Chunk: + """A circuit chunk with accompanying stabilizer flow assertions. + """ +``` + + +```python +# stimflow.Chunk.__init__ + +# (in class stimflow.Chunk) +def __init__( + self, + circuit: stim.Circuit, + *, + flows: Iterable[Flow], + discarded_inputs: Iterable[PauliMap | Tile] = (), + discarded_outputs: Iterable[PauliMap | Tile] = (), + wants_to_merge_with_next: bool = False, + wants_to_merge_with_prev: bool = False, + q2i: dict[complex, int] | None = None, + o2i: dict[Any, int] | None = None, +): + """ + + Args: + circuit: The circuit implementing the chunk's functionality. + flows: A series of stabilizer flows that the circuit implements. + discarded_inputs: Explicitly rejected in flows. For example, a data + measurement chunk might reject flows for stabilizers from the + anticommuting basis. If they are not rejected, then compilation + will fail when attempting to combine this chunk with a preceding + chunk that has those stabilizers from the anticommuting basis + flowing out. + discarded_outputs: Explicitly rejected out flows. For example, an + initialization chunk might reject flows for stabilizers from the + anticommuting basis. If they are not rejected, then compilation + will fail when attempting to combine this chunk with a following + chunk that has those stabilizers from the anticommuting basis + flowing in. + wants_to_merge_with_next: Defaults to False. When set to True, + the chunk compiler won't insert a TICK between this chunk + and the next chunk. + wants_to_merge_with_prev: Defaults to False. When set to True, + the chunk compiler won't insert a TICK between this chunk + and the previous chunk. + q2i: Defaults to None (infer from QUBIT_COORDS instructions in circuit else + raise an exception). The stimflow-qubit-coordinate-to-stim-qubit-index mapping + used to translate between stimflow's qubit keys and stim's qubit keys. + o2i: Defaults to None (raise an exception if observables present in circuit). + The stimflow-observable-key-to-stim-observable-index mapping used to translate + between stimflow's observable keys and stim's observable keys. + """ +``` + + +```python +# stimflow.Chunk.end_code + +# (in class stimflow.Chunk) +def end_code( + self, +) -> StabilizerCode: +``` + + +```python +# stimflow.Chunk.end_interface + +# (in class stimflow.Chunk) +def end_interface( + self, + *, + skip_passthroughs: bool = False, +) -> ChunkInterface: + """Returns a description of the flows that should exit from the chunk. + """ +``` + + +```python +# stimflow.Chunk.end_patch + +# (in class stimflow.Chunk) +def end_patch( + self, +) -> Patch: +``` + + +```python +# stimflow.Chunk.find_distance + +# (in class stimflow.Chunk) +def find_distance( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 0.001, + noiseless_qubits: Iterable[float | int | complex] = (), + skip_adding_noise: bool = False, +) -> int: +``` + + +```python +# stimflow.Chunk.find_logical_error + +# (in class stimflow.Chunk) +def find_logical_error( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 0.001, + noiseless_qubits: Iterable[float | int | complex] = (), + skip_adding_noise: bool = False, +) -> list[stim.ExplainedError]: +``` + + +```python +# stimflow.Chunk.flattened + +# (in class stimflow.Chunk) +def flattened( + self, +) -> list[Chunk]: + """This is here for duck-type compatibility with ChunkLoop. + """ +``` + + +```python +# stimflow.Chunk.from_circuit_with_mpp_boundaries + +# (in class stimflow.Chunk) +def from_circuit_with_mpp_boundaries( + circuit: stim.Circuit, +) -> Chunk: +``` + + +```python +# stimflow.Chunk.start_code + +# (in class stimflow.Chunk) +def start_code( + self, +) -> StabilizerCode: +``` + + +```python +# stimflow.Chunk.start_interface + +# (in class stimflow.Chunk) +def start_interface( + self, + *, + skip_passthroughs: bool = False, +) -> ChunkInterface: + """Returns a description of the flows that should enter into the chunk. + """ +``` + + +```python +# stimflow.Chunk.start_patch + +# (in class stimflow.Chunk) +def start_patch( + self, +) -> Patch: +``` + + +```python +# stimflow.Chunk.then + +# (in class stimflow.Chunk) +def then( + self, + other: Chunk | ChunkReflow | ChunkLoop, +) -> Chunk: +``` + + +```python +# stimflow.Chunk.time_reversed + +# (in class stimflow.Chunk) +def time_reversed( + self, +) -> Chunk: + """Checks that this chunk's circuit actually implements its flows. + """ +``` + + +```python +# stimflow.Chunk.to_closed_circuit + +# (in class stimflow.Chunk) +def to_closed_circuit( + self, +) -> stim.Circuit: + """Compiles the chunk into a circuit by conjugating with mpp init/end chunks. + """ +``` + + +```python +# stimflow.Chunk.to_coord_circuit + +# (in class stimflow.Chunk) +def to_coord_circuit( + self, +) -> stim.Circuit: +``` + + +```python +# stimflow.Chunk.to_html_viewer + +# (in class stimflow.Chunk) +def to_html_viewer( + self, + *, + background: Patch | StabilizerCode | ChunkInterface | dict[int, Patch | StabilizerCode | ChunkInterface] | None = None, + tile_color_func: Callable[[Tile], tuple[float, float, float, float]] | None = None, + known_error: Iterable[stim.ExplainedError] | None = None, +) -> str_html: +``` + + +```python +# stimflow.Chunk.verify + +# (in class stimflow.Chunk) +def verify( + self, + *, + expected_in: ChunkInterface | StabilizerCode | Patch | None = None, + expected_out: ChunkInterface | StabilizerCode | Patch | None = None, + should_measure_all_code_stabilizers: bool = False, + allow_overlapping_flows: bool = False, +): + """Checks that this chunk's circuit actually implements its flows. + """ +``` + + +```python +# stimflow.Chunk.verify_distance_is_at_least_2 + +# (in class stimflow.Chunk) +def verify_distance_is_at_least_2( + self, + *, + noise: float | NoiseModel = 0.001, +): + """Verifies undetected logical errors require at least 2 physical errors. + + By default, verifies using a uniform depolarizing circuit noise model. + """ +``` + + +```python +# stimflow.Chunk.verify_distance_is_at_least_3 + +# (in class stimflow.Chunk) +def verify_distance_is_at_least_3( + self, + *, + noise: float | NoiseModel = 0.001, +): + """Verifies undetected logical errors require at least 3 physical errors. + + By default, verifies using a uniform depolarizing circuit noise model. + """ +``` + + +```python +# stimflow.Chunk.with_edits + +# (in class stimflow.Chunk) +def with_edits( + self, + *, + circuit: stim.Circuit | None = None, + q2i: dict[complex, int] | None = None, + flows: Iterable[Flow] | None = None, + discarded_inputs: Iterable[PauliMap] | None = None, + discarded_outputs: Iterable[PauliMap] | None = None, + wants_to_merge_with_prev: bool | None = None, + wants_to_merge_with_next: bool | None = None, +) -> Chunk: +``` + + +```python +# stimflow.Chunk.with_flag_added_to_all_flows + +# (in class stimflow.Chunk) +def with_flag_added_to_all_flows( + self, + flag: str, +) -> Chunk: +``` + + +```python +# stimflow.Chunk.with_obs_flows_as_det_flows + +# (in class stimflow.Chunk) +def with_obs_flows_as_det_flows( + self, +) -> Chunk: +``` + + +```python +# stimflow.Chunk.with_repetitions + +# (in class stimflow.Chunk) +def with_repetitions( + self, + repetitions: int, +) -> ChunkLoop: +``` + + +```python +# stimflow.Chunk.with_transformed_coords + +# (in class stimflow.Chunk) +def with_transformed_coords( + self, + transform: Callable[[complex], complex], +) -> Chunk: +``` + + +```python +# stimflow.Chunk.with_xz_flipped + +# (in class stimflow.Chunk) +def with_xz_flipped( + self, +) -> Chunk: +``` + + +```python +# stimflow.ChunkBuilder + +# (at top-level in the stimflow module) +class ChunkBuilder: + """Helper class for building stim circuits. + + Handles qubit indexing (complex -> int). + Handles measurement tracking (naming results and referring to them by name). + """ +``` + + +```python +# stimflow.ChunkBuilder.__init__ + +# (in class stimflow.ChunkBuilder) +def __init__( + self, + allowed_qubits: Iterable[complex] | None = None, +): + """Creates a Builder for creating a circuit over the given qubits. + + Args: + allowed_qubits: Defaults to None (everything allowed). Specifies the qubit positions + that the circuit is permitted to contain. + """ +``` + + +```python +# stimflow.ChunkBuilder.add_discarded_flow_input + +# (in class stimflow.ChunkBuilder) +def add_discarded_flow_input( + self, + flow: PauliMap | Tile, +) -> None: +``` + + +```python +# stimflow.ChunkBuilder.add_discarded_flow_output + +# (in class stimflow.ChunkBuilder) +def add_discarded_flow_output( + self, + flow: PauliMap | Tile, +) -> None: +``` + + +```python +# stimflow.ChunkBuilder.add_flow + +# (in class stimflow.ChunkBuilder) +def add_flow( + self, + *, + start: "PauliMap | Tile | Literal['auto'] | None" = None, + end: "PauliMap | Tile | Literal['auto'] | None" = None, + ms: "Iterable[Any] | Literal['auto']" = (), + ignore_unmatched_ms: bool = False, + obs_key: Any = None, + center: complex | None = None, + flags: Iterable[str] = frozenset(), + sign: bool | None = None, +) -> None: + """Declares that the circuit being built should have a given stabilizer flow. + + When chunks are concatenated, their flows are paired up in order to form detectors. + + Args: + start: Defaults to None (empty). The stabilizer that the flow starts as, at the + beginning of the circuit. If the flow begins within the circuit, this should + be set to None or an empty PauliMap. + end: Defaults to None (empty). The stabilizer that the flow ends as, at the + end of the circuit. If the flow ends within the circuit, this should + be set to None or an empty PauliMap. + ms: Defaults to empty. The keys identifying measurements mediate the flow. + For example, if a stabilizer is measured by a circuit then this would + typically be a singleton list containing the measurement that reveals + the stabilizer's value. + ignore_unmatched_ms: Defaults to False. When set to False, unrecognized measurement + ids cause the method to raise an exception instead of adding the flow. When set + to True, unrecognized measurements are silently discarded. + obs_key: Defaults to None (not a logical operator). If this is set to a value other + than None, it identifies the logical operator whose flow the flow is describing. + center: Defaults to None (unused). Optional metadata specifying coordinates for the + flow. Typically these coordinates will end up being exposed as the parens args + on the DETECTOR instruction created when producing a stim circuit. When not + specified, the coordinates will instead be inferred in some heuristic way. + flags: Defaults to empty. Hashable equatable values associated with the flow. When + flows are combined, the result will contain the union of their flags. When compiling + chunks into a circuit, the optional `metadata_func` argument can use these flags + to produce better metadata. + sign: Defaults to None (unsigned). When not set, the circuit having the flow with either + a positive or negative sign are both acceptable. When set to False or True, the sign + implemented by the circuit must match. + + Examples: + >>> import stimflow as sf + >>> builder = sf.ChunkBuilder() + >>> builder.append('R', [0]) + >>> builder.append('MX', [1j]) + >>> builder.append('TICK') + >>> builder.append('CX', [(1j, 0)]) + + >>> builder.add_flow(end=sf.PauliMap.from_xs([0, 1j]), ms=[1j]) + >>> builder.add_flow(end=sf.PauliMap.from_zs([0, 1j])) + >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), ms=[1j]) + + >>> builder.finish_chunk().verify() + """ +``` + + +```python +# stimflow.ChunkBuilder.append + +# (in class stimflow.ChunkBuilder) +def append( + self, + gate: str, + targets: Iterable[complex | Sequence[complex] | PauliMap | Tile | Any] = (), + *, + arg: float | Iterable[float] | None = None, + measure_key_func: Callable[[complex], Any] | Callable[[tuple[complex, complex]], Any] | Callable[[PauliMap | Tile], Any] | None = lambda e: e, + tag: str = ', + unknown_qubit_append_mode: "Literal['auto, 'error, 'skip, 'include']" = 'auto, +) -> None: + """Appends an instruction to the builder's circuit. + + This method differs from `stim.Circuit.append` in the following ways: + + 1) It targets qubits by position instead of by index. Also, it takes two + qubit targets as pairs instead of interleaved. For example, instead of + saying + + a = builder.q2i[5 + 1j] + b = builder.q2i[5] + c = builder.q2i[0] + d = builder.q2i[1j] + builder.circuit.append('CZ', [a, b, c, d]) + + you would say + + builder.append('CZ', [(5+1j, 5), (0, 1j)]) + + 2) It canonicalizes. In particular, it will: + - Sort targets. For example: + `H 3 1 2` -> `H 1 2 3` + `CX 2 3 1 0` -> `CX 1 0 2 3` + `CZ 2 3 6 0` -> `CZ 0 6 2 3` + - Replace rare gates with common gates. For example: + `XCZ 1 2` -> `CX 2 1` + - Not append target-less gates at all. For example: + `CX ` -> `` + + Canonicalization makes the form of the final circuit stable, + despite things like python's `set` data structure having + inconsistent iteration orders. This makes the output easier + to unit test, and more viable to store under source control. + + 3) It tracks measurements. When appending a measurement, its index is + stored in the measurement tracker keyed by the position of the qubit + being measured (or by a custom key, if `measure_key_func` is specified). + The indices of the measurements can be looked up later via + `builder.lookup_mids([key1, key2, ...])`. + + Args: + gate: The name of the gate to append, such as "H" or "M" or "CX". + targets: The qubit positions that the gate operates on. For single + qubit gates like H or M this should be an iterable of complex + numbers. For two qubit gates like CX or MXX it should be an + iterable of pairs of complex numbers. For MPP it should be an + iterable of stimflow.PauliMap instances. + arg: Optional. The parens argument or arguments used for the gate + instruction. For example, for a measurement gate, this is the + probability of the incorrect result being reported. + measure_key_func: Customizes the keys used to track the indices of + measurement results. By default, measurements are keyed by + position, but thus won't work if a circuit measures the same + qubit multiple times. This function can transform that position + into a different value (for example, you might set + `measure_key_func=lambda pos: (pos, 'first_cycle')` for + measurements during the first cycle of the circuit. + tag: Defaults to "" (no tag). A custom tag to attach to the + instruction(s) appended into the stim circuit. + unknown_qubit_append_mode: Defaults to 'auto'. The available options are: + - 'auto': Replace by 'include' if the builder's `allowed_qubits` field is + empty, else replace by 'error'. + - 'error': When a qubit position outside `allowed_qubits` is encountered, + raise an exception. + - 'include': When a qubit position outside `allowed_qubits` is encountered, + automatically include it into `builder.q2i` and `builder.allowed_qubits`. + - 'skip': When a qubit position outside `allowed_qubits` is encountered, + ignore it. Note that, for two-qubit and multi-qubit operations, this + will ignore the pair or group of targets containing the skipped position. + """ +``` + + +```python +# stimflow.ChunkBuilder.append_feedback + +# (in class stimflow.ChunkBuilder) +def append_feedback( + self, + *, + control_keys: Iterable[Any], + targets: Iterable[complex], + basis: str, + unknown_qubit_append_mode: "Literal['auto, 'error, 'skip, 'include']" = 'auto, +) -> None: + """Appends the tensor product of the given controls and targets into the circuit. + """ +``` + + +```python +# stimflow.ChunkBuilder.finish_chunk + +# (in class stimflow.ChunkBuilder) +def finish_chunk( + self, + *, + wants_to_merge_with_prev: bool = False, + wants_to_merge_with_next: bool = False, + failure_mode: "Literal['error, 'ignore, 'print']" = 'error, +) -> Chunk: + """Finishes producing the circuit. + """ +``` + + +```python +# stimflow.ChunkBuilder.has_measurement + +# (in class stimflow.ChunkBuilder) +def has_measurement( + self, + key: Any, +) -> bool: +``` + + +```python +# stimflow.ChunkBuilder.lookup_mids + +# (in class stimflow.ChunkBuilder) +def lookup_mids( + self, + keys: Iterable[Any], + *, + ignore_unmatched: bool = False, +) -> list[int]: + """Looks up measurement indices by key. + + Measurement keys are created automatically when appending measurement operations into the + circuit via the builder's append method. They are also created manually by methods like + `builder.record_measurement_group`. + + Args: + keys: The measurement keys to lookup. + ignore_unmatched: Defaults to False. If set to True, keys that don't correspond + to measurements are ignored instead of raising an error. + + Returns: + A list of offsets indicating when the measurements occurred. + """ +``` + + +```python +# stimflow.ChunkBuilder.record_measurement_group + +# (in class stimflow.ChunkBuilder) +def record_measurement_group( + self, + sub_keys: Iterable[Any], + *, + key: Any, +) -> None: + """Combines multiple measurement keys into one key. + + Args: + sub_keys: The measurement keys to combine. + key: Where to store the combined result. + """ +``` + + +```python +# stimflow.ChunkCompiler + +# (at top-level in the stimflow module) +class ChunkCompiler: + """Compiles appended chunks into a unified circuit. + """ +``` + + +```python +# stimflow.ChunkCompiler.__init__ + +# (in class stimflow.ChunkCompiler) +def __init__( + self, + *, + metadata_func: Callable[[Flow], FlowMetadata] | None = None, +): + """ + + Args: + metadata_func: Determines coordinate data appended to detectors + (after x, y, and t). Defaults to None (no extra metadata). + """ +``` + + +```python +# stimflow.ChunkCompiler.append + +# (in class stimflow.ChunkCompiler) +def append( + self, + appended: Chunk | ChunkLoop | ChunkReflow, +) -> None: + """Appends a chunk to the circuit being built. + + The input flows of the appended chunk must exactly match the open outgoing flows of the + circuit so far. + """ +``` + + +```python +# stimflow.ChunkCompiler.append_magic_end_chunk + +# (in class stimflow.ChunkCompiler) +def append_magic_end_chunk( + self, + expected: ChunkInterface | None = None, +) -> None: + """Appends a non-physical chunk that terminates the circuit, regardless of open flows. + + Args: + expected: Defaults to None (unused). If set to None, no extra checks are performed. + If set to a ChunkInterface, it is verified that the open flows actually + correspond to this interface. + """ +``` + + +```python +# stimflow.ChunkCompiler.append_magic_init_chunk + +# (in class stimflow.ChunkCompiler) +def append_magic_init_chunk( + self, + expected: ChunkInterface | None = None, +) -> None: + """Appends a non-physical chunk that outputs the flows expected by the next chunk. + + Args: + expected: Defaults to None (unused). If set to a ChunkInterface, it will be + verified that the next appended chunk actually has a start interface + matching the given expected interface. If set to None, then no checks are + performed; no constraints are placed on the next chunk. + """ +``` + + +```python +# stimflow.ChunkCompiler.copy + +# (in class stimflow.ChunkCompiler) +def copy( + self, +) -> ChunkCompiler: + """Returns a deep copy of the compiler's state. + """ +``` + + +```python +# stimflow.ChunkCompiler.cur_circuit_html_viewer + +# (in class stimflow.ChunkCompiler) +def cur_circuit_html_viewer( + self, +) -> stimflow.str_html: +``` + + +```python +# stimflow.ChunkCompiler.cur_end_interface + +# (in class stimflow.ChunkCompiler) +def cur_end_interface( + self, +) -> ChunkInterface: +``` + + +```python +# stimflow.ChunkCompiler.ensure_observables_included + +# (in class stimflow.ChunkCompiler) +def ensure_observables_included( + self, + observable_names: Iterable[Any], +): +``` + + +```python +# stimflow.ChunkCompiler.ensure_qubits_included + +# (in class stimflow.ChunkCompiler) +def ensure_qubits_included( + self, + qubits: Iterable[complex], +): + """Adds the given qubit positions to the indexed positions, if they aren't already. + """ +``` + + +```python +# stimflow.ChunkCompiler.finish_circuit + +# (in class stimflow.ChunkCompiler) +def finish_circuit( + self, +) -> stim.Circuit: + """Returns the circuit built by the compiler. + + Performs some final translation steps: + - Re-indexing the qubits to be in a sorted order. + - Re-indexing the observables to omit discarded observable flows. + """ +``` + + +```python +# stimflow.ChunkInterface + +# (at top-level in the stimflow module) +class ChunkInterface: + """Specifies a set of stabilizers and observables that a chunk can consume or prepare. + """ +``` + + +```python +# stimflow.ChunkInterface.data_set + +# (in class stimflow.ChunkInterface) +class data_set: +``` + + +```python +# stimflow.ChunkInterface.partitioned_detector_flows + +# (in class stimflow.ChunkInterface) +def partitioned_detector_flows( + self, +) -> list[list[PauliMap]]: + """Returns the stabilizers of the interface, split into non-overlapping groups. + """ +``` + + +```python +# stimflow.ChunkInterface.to_code + +# (in class stimflow.ChunkInterface) +def to_code( + self, +) -> StabilizerCode: + """Returns a stimflow.StabilizerCode with an equivalent interface. + """ +``` + + +```python +# stimflow.ChunkInterface.to_patch + +# (in class stimflow.ChunkInterface) +def to_patch( + self, +) -> Patch: + """Returns a stimflow.Patch with tiles equal to the chunk interface's stabilizers. + """ +``` + + +```python +# stimflow.ChunkInterface.to_svg + +# (in class stimflow.ChunkInterface) +def to_svg( + self, + *, + show_order: bool = False, + show_measure_qubits: bool = False, + show_data_qubits: bool = True, + system_qubits: Iterable[complex] = (), + opacity: float = 1, + show_coords: bool = True, + show_obs: bool = True, + other: StabilizerCode | Patch | Iterable[StabilizerCode | Patch] | None = None, + tile_color_func: Callable[[Tile], str] | None = None, + rows: int | None = None, + cols: int | None = None, + find_logical_err_max_weight: int | None = None, +) -> str_svg: +``` + + +```python +# stimflow.ChunkInterface.used_set + +# (in class stimflow.ChunkInterface) +class used_set: + """Returns the set of qubits used in any flow mentioned by the chunk interface. + """ +``` + + +```python +# stimflow.ChunkInterface.with_discards_as_ports + +# (in class stimflow.ChunkInterface) +def with_discards_as_ports( + self, +) -> ChunkInterface: + """Returns the same chunk interface, but with discarded flows turned into normal flows. + """ +``` + + +```python +# stimflow.ChunkInterface.with_edits + +# (in class stimflow.ChunkInterface) +def with_edits( + self, + *, + ports: Iterable[PauliMap] | None = None, + discards: Iterable[PauliMap] | None = None, +) -> ChunkInterface: + """Returns an equivalent chunk interface but with the given values replaced. + """ +``` + + +```python +# stimflow.ChunkInterface.with_transformed_coords + +# (in class stimflow.ChunkInterface) +def with_transformed_coords( + self, + transform: Callable[[complex], complex], +) -> ChunkInterface: + """Returns the same interface, but with coordinates transformed by the given function. + """ +``` + + +```python +# stimflow.ChunkInterface.without_discards + +# (in class stimflow.ChunkInterface) +def without_discards( + self, +) -> ChunkInterface: + """Returns the same chunk interface, but with discarded flows not included. + """ +``` + + +```python +# stimflow.ChunkInterface.without_keyed + +# (in class stimflow.ChunkInterface) +def without_keyed( + self, +) -> ChunkInterface: + """Returns the same chunk interface, but without logical flows (named flows). + """ +``` + + +```python +# stimflow.ChunkLoop + +# (at top-level in the stimflow module) +class ChunkLoop: + """Specifies a series of chunks to repeat a fixed number of times. + + The loop invariant is that the last chunk's end interface should match the + first chunk's start interface (unless the number of repetitions is less than + 2). + + For duck typing purposes, many methods supported by Chunk are supported by + ChunkLoop. + """ +``` + + +```python +# stimflow.ChunkLoop.end_interface + +# (in class stimflow.ChunkLoop) +def end_interface( + self, +) -> ChunkInterface: + """Returns the end interface of the last chunk in the loop. + """ +``` + + +```python +# stimflow.ChunkLoop.end_patch + +# (in class stimflow.ChunkLoop) +def end_patch( + self, +) -> Patch: +``` + + +```python +# stimflow.ChunkLoop.find_distance + +# (in class stimflow.ChunkLoop) +def find_distance( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 0.001, + noiseless_qubits: Iterable[float | int | complex] = (), +) -> int: +``` + + +```python +# stimflow.ChunkLoop.find_logical_error + +# (in class stimflow.ChunkLoop) +def find_logical_error( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 0.001, + noiseless_qubits: Iterable[float | int | complex] = (), +) -> list[stim.ExplainedError]: + """Searches for a minium distance undetected logical error. + + By default, searches using a uniform depolarizing circuit noise model. + """ +``` + + +```python +# stimflow.ChunkLoop.flattened + +# (in class stimflow.ChunkLoop) +def flattened( + self, +) -> list[Chunk | ChunkReflow]: + """Unrolls the loop, and any sub-loops, into a series of chunks. + """ +``` + + +```python +# stimflow.ChunkLoop.start_interface + +# (in class stimflow.ChunkLoop) +def start_interface( + self, +) -> ChunkInterface: + """Returns the start interface of the first chunk in the loop. + """ +``` + + +```python +# stimflow.ChunkLoop.start_patch + +# (in class stimflow.ChunkLoop) +def start_patch( + self, +) -> Patch: +``` + + +```python +# stimflow.ChunkLoop.time_reversed + +# (in class stimflow.ChunkLoop) +def time_reversed( + self, +) -> ChunkLoop: + """Returns the same loop, but time reversed. + + The time reversed loop has reversed flows, implemented by performs operations in the + reverse order and exchange measurements for resets (and vice versa) as appropriate. + It has exactly the same fault tolerant structure, just mirrored in time. + """ +``` + + +```python +# stimflow.ChunkLoop.to_closed_circuit + +# (in class stimflow.ChunkLoop) +def to_closed_circuit( + self, +) -> stim.Circuit: + """Compiles the chunk into a circuit by conjugating with mpp init/end chunks. + """ +``` + + +```python +# stimflow.ChunkLoop.to_html_viewer + +# (in class stimflow.ChunkLoop) +def to_html_viewer( + self, + *, + patch: Patch | StabilizerCode | ChunkInterface | None = None, + tile_color_func: Callable[[Tile], tuple[float, float, float, float]] | None = None, + known_error: Iterable[stim.ExplainedError] | None = None, +) -> str_html: + """Returns an HTML document containing a viewer for the chunk loop's circuit. + """ +``` + + +```python +# stimflow.ChunkLoop.verify + +# (in class stimflow.ChunkLoop) +def verify( + self, + *, + expected_in: ChunkInterface | None = None, + expected_out: ChunkInterface | None = None, +): +``` + + +```python +# stimflow.ChunkLoop.verify_distance_is_at_least_2 + +# (in class stimflow.ChunkLoop) +def verify_distance_is_at_least_2( + self, + *, + noise: float | NoiseModel = 0.001, +): + """Verifies undetected logical errors require at least 2 physical errors. + + Verifies using a uniform depolarizing circuit noise model. + """ +``` + + +```python +# stimflow.ChunkLoop.verify_distance_is_at_least_3 + +# (in class stimflow.ChunkLoop) +def verify_distance_is_at_least_3( + self, + *, + noise: float | NoiseModel = 0.001, +): + """Verifies undetected logical errors require at least 3 physical errors. + + By default, verifies using a uniform depolarizing circuit noise model. + """ +``` + + +```python +# stimflow.ChunkLoop.with_repetitions + +# (in class stimflow.ChunkLoop) +def with_repetitions( + self, + new_repetitions: int, +) -> ChunkLoop: + """Returns the same loop, but with a different number of repetitions. + """ +``` + + +```python +# stimflow.ChunkReflow + +# (at top-level in the stimflow module) +class ChunkReflow: + """An adapter chunk for attaching chunks describing the same thing in different ways. + + For example, consider two surface code idle round chunks where one has the logical + operator on the left side and the other has the logical operator on the right side. + They can't be directly concatenated, because their flows don't match. But a reflow + chunk can be placed in between, mapping the left logical operator to the right + logical operator times a set of stabilizers, in order to bridge the incompatibility. + """ +``` + + +```python +# stimflow.ChunkReflow.end_code + +# (in class stimflow.ChunkReflow) +def end_code( + self, +) -> StabilizerCode: +``` + + +```python +# stimflow.ChunkReflow.end_interface + +# (in class stimflow.ChunkReflow) +def end_interface( + self, +) -> ChunkInterface: +``` + + +```python +# stimflow.ChunkReflow.end_patch + +# (in class stimflow.ChunkReflow) +def end_patch( + self, +) -> Patch: +``` + + +```python +# stimflow.ChunkReflow.flattened + +# (in class stimflow.ChunkReflow) +def flattened( + self, +) -> list[ChunkReflow]: + """This is here for duck-type compatibility with ChunkLoop. + """ +``` + + +```python +# stimflow.ChunkReflow.from_auto_rewrite + +# (in class stimflow.ChunkReflow) +def from_auto_rewrite( + *, + inputs: Iterable[PauliMap], + out2in: "dict[PauliMap, list[PauliMap] | Literal['auto']]", +) -> ChunkReflow: +``` + + +```python +# stimflow.ChunkReflow.from_auto_rewrite_transitions_using_stable + +# (in class stimflow.ChunkReflow) +def from_auto_rewrite_transitions_using_stable( + *, + stable: Iterable[PauliMap], + transitions: Iterable[tuple[PauliMap, PauliMap]], +) -> ChunkReflow: + """Bridges the given transitions using products from the given stable values. + """ +``` + + +```python +# stimflow.ChunkReflow.removed_inputs + +# (in class stimflow.ChunkReflow) +class removed_inputs: +``` + + +```python +# stimflow.ChunkReflow.start_code + +# (in class stimflow.ChunkReflow) +def start_code( + self, +) -> StabilizerCode: +``` + + +```python +# stimflow.ChunkReflow.start_interface + +# (in class stimflow.ChunkReflow) +def start_interface( + self, +) -> ChunkInterface: +``` + + +```python +# stimflow.ChunkReflow.start_patch + +# (in class stimflow.ChunkReflow) +def start_patch( + self, +) -> Patch: +``` + + +```python +# stimflow.ChunkReflow.verify + +# (in class stimflow.ChunkReflow) +def verify( + self, + *, + expected_in: StabilizerCode | ChunkInterface | None = None, + expected_out: StabilizerCode | ChunkInterface | None = None, +): + """Verifies that the ChunkReflow is well-formed. + """ +``` + + +```python +# stimflow.ChunkReflow.with_obs_flows_as_det_flows + +# (in class stimflow.ChunkReflow) +def with_obs_flows_as_det_flows( + self, +): +``` + + +```python +# stimflow.ChunkReflow.with_transformed_coords + +# (in class stimflow.ChunkReflow) +def with_transformed_coords( + self, + transform: Callable[[complex], complex], +) -> ChunkReflow: +``` + + +```python +# stimflow.Flow + +# (at top-level in the stimflow module) +class Flow: + """A rule for how a stabilizer travels into, through, and/or out of a circuit. + """ +``` + + +```python +# stimflow.Flow.__init__ + +# (in class stimflow.Flow) +def __init__( + self, + *, + start: PauliMap | Tile | None = None, + end: PauliMap | Tile | None = None, + mids: Iterable[int] = (), + obs_key: Any = None, + center: complex | None = None, + flags: Iterable[Any] = frozenset(), + sign: bool | None = None, +): + """Initializes a Flow. + + Args: + start: Defaults to None (empty). The Pauli product operator at the beginning of the + circuit (before *all* operations, including resets). + end: Defaults to None (empty). The Pauli product operator at the end of the + circuit (after *all* operations, including measurements). + mids: Defaults to empty. Indices of measurements that mediate the flow (that multiply + into it as it traverses the circuit). + center: Defaults to None (unspecified). Specifies a 2d coordinate to use in metadata + when the flow is completed into a detector. Incompatible with obs_key. + obs_key: Defaults to None (detector flow). Identifies that this is an observable flow + (instead of a detector flow) and gives a name that be used when linking chunks. + flags: Defaults to empty. Custom information about the flow, that can be used by code + operating on chunks for a variety of purposes. For example, this could identify the + "color" of the flow in a color code. + sign: Defaults to None (unsigned). The expected sign of the flow. + """ +``` + + +```python +# stimflow.Flow.__mul__ + +# (in class stimflow.Flow) +def __mul__( + self, + other: Flow, +) -> Flow: + """Computes the product of two flows. + + The product of A -> B and C -> D is (A*C) -> (B*D). + """ +``` + + +```python +# stimflow.Flow.fuse_with_next_flow + +# (in class stimflow.Flow) +def fuse_with_next_flow( + self, + next_flow: Flow, + *, + next_flow_measure_offset: int, +) -> Flow: +``` + + +```python +# stimflow.Flow.obs_key + +# (in class stimflow.Flow) +@property +def obs_key( + self, +): +``` + + +```python +# stimflow.Flow.to_stim_flow + +# (in class stimflow.Flow) +def to_stim_flow( + self, + *, + q2i: dict[complex, int], + o2i: Mapping[Any, int | None] | None = None, +) -> stim.Flow: +``` + + +```python +# stimflow.Flow.with_edits + +# (in class stimflow.Flow) +def with_edits( + self, + *, + start: PauliMap = , + end: PauliMap = , + measurement_indices: Iterable[int] = , + obs_key: Any = , + center: complex = , + flags: Iterable[str] = , + sign: Any = , +) -> Flow: +``` + + +```python +# stimflow.Flow.with_transformed_coords + +# (in class stimflow.Flow) +def with_transformed_coords( + self, + transform: Callable[[complex], complex], +) -> Flow: +``` + + +```python +# stimflow.Flow.with_xz_flipped + +# (in class stimflow.Flow) +def with_xz_flipped( + self, +) -> Flow: +``` + + +```python +# stimflow.FlowMetadata + +# (at top-level in the stimflow module) +class FlowMetadata: + """Metadata, based on a flow, to use during circuit generation. + """ +``` + + +```python +# stimflow.FlowMetadata.__init__ + +# (in class stimflow.FlowMetadata) +def __init__( + self, + *, + extra_coords: Iterable[float] = (), + tag: str | None = ', +): + """ + + Args: + extra_coords: Extra numbers to add to DETECTOR coordinate arguments. By default stimflow + gives each detector an X, Y, and T coordinate. These numbers go afterward. + tag: A tag to attach to DETECTOR or OBSERVABLE_INCLUDE instructions. + """ +``` + + +```python +# stimflow.InteractLayer + +# (at top-level in the stimflow module) +@dataclasses.dataclass +class InteractLayer: + """A layer of controlled Pauli gates (like CX, CZ, and XCY). + """ + targets1: list[int] + targets2: list[int] + bases1: list[str] + bases2: list[str] +``` + + +```python +# stimflow.InteractLayer.append_into_stim_circuit + +# (in class stimflow.InteractLayer) +def append_into_stim_circuit( + self, + out: stim.Circuit, +) -> None: +``` + + +```python +# stimflow.InteractLayer.copy + +# (in class stimflow.InteractLayer) +def copy( + self, +) -> InteractLayer: +``` + + +```python +# stimflow.InteractLayer.locally_optimized + +# (in class stimflow.InteractLayer) +def locally_optimized( + self, + next_layer: Layer | None, +) -> list[Layer | None]: +``` + + +```python +# stimflow.InteractLayer.rotate_to_z_layer + +# (in class stimflow.InteractLayer) +def rotate_to_z_layer( + self, +): +``` + + +```python +# stimflow.InteractLayer.to_z_basis + +# (in class stimflow.InteractLayer) +def to_z_basis( + self, +) -> list[Layer]: +``` + + +```python +# stimflow.InteractLayer.touched + +# (in class stimflow.InteractLayer) +def touched( + self, +) -> set[int]: +``` + + +```python +# stimflow.LayerCircuit + +# (at top-level in the stimflow module) +@dataclasses.dataclass +class LayerCircuit: + """A stabilizer circuit represented as a series of typed layers. + + For example, the circuit could be a `ResetLayer`, then a `RotationLayer`, + then a few `InteractLayer`s, then a `MeasureLayer`. + """ + layers: list[Layer] +``` + + +```python +# stimflow.LayerCircuit.copy + +# (in class stimflow.LayerCircuit) +def copy( + self, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.from_stim_circuit + +# (in class stimflow.LayerCircuit) +def from_stim_circuit( + circuit: stim.Circuit, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.to_stim_circuit + +# (in class stimflow.LayerCircuit) +def to_stim_circuit( + self, +) -> stim.Circuit: + """Compiles the layer circuit into a stim circuit and returns it. + """ +``` + + +```python +# stimflow.LayerCircuit.to_z_basis + +# (in class stimflow.LayerCircuit) +def to_z_basis( + self, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.touched + +# (in class stimflow.LayerCircuit) +def touched( + self, +) -> set[int]: +``` + + +```python +# stimflow.LayerCircuit.with_cleaned_up_loop_iterations + +# (in class stimflow.LayerCircuit) +def with_cleaned_up_loop_iterations( + self, +) -> LayerCircuit: + """Attempts to roll up partially unrolled loops. + + Checks if the instructions before a loop correspond to the instruction inside a loop. If so, + removes the matching instructions beforehand and increases the iteration count by 1. Same + for instructions after the loop. + + This essentially undoes the effect of `with_ejected_loop_iterations`. A common pattern is + to do `with_ejected_loop_iterations`, then an optimization, then + `with_cleaned_up_loop_iterations`. This gives the optimization the chance to optimize across + a loop boundary, but cleans up after itself if no optimization occurs. + + In some cases this method is useful because of circuit generation code being overly cautious + about how quickly loop invariants are established, and so emitting the first iteration of a + loop in a special way. If it happens to be identical, despite the different code path that + produced it, this method will roll it into the rest of the loop. + + For example, this method would turn this circuit fragment: + + X 0 + MR 0 + REPEAT 98 { + X 0 + MR 0 + } + X 0 + MR 0 + + into this circuit fragment: + + REPEAT 100 { + X 0 + MR 0 + } + """ +``` + + +```python +# stimflow.LayerCircuit.with_clearable_rotation_layers_cleared + +# (in class stimflow.LayerCircuit) +def with_clearable_rotation_layers_cleared( + self, +) -> LayerCircuit: + """Removes rotation layers where every rotation in the layer can be moved to another layer. + + Each individual rotation can move through intermediate non-rotation layers as long as those + layers don't touch the qubit being rotated. + """ +``` + + +```python +# stimflow.LayerCircuit.with_ejected_loop_iterations + +# (in class stimflow.LayerCircuit) +def with_ejected_loop_iterations( + self, +) -> LayerCircuit: + """Partially unrolls loops, placing one iteration before and one iteration after. + + This is useful for ensuring the transition into and out of a loop is optimized correctly. + For example, if a circuit begins with a transversal initialization of data qubits and then + immediately starts a memory loop, the resets from the data initialization should be merged + into the same layer as the resets from the measurement initialization at the beginning of + the loop. But the reset-merging optimization might not see that this is possible across the + loop boundary. Ejecting an iteration fixes this issue. + + For example, this method would turn this circuit fragment: + + REPEAT 100 { + X 0 + MR 0 + } + + into this circuit fragment: + + X 0 + MR 0 + REPEAT 98 { + X 0 + MR 0 + } + X 0 + MR 0 + """ +``` + + +```python +# stimflow.LayerCircuit.with_irrelevant_tail_layers_removed + +# (in class stimflow.LayerCircuit) +def with_irrelevant_tail_layers_removed( + self, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.with_locally_merged_measure_layers + +# (in class stimflow.LayerCircuit) +def with_locally_merged_measure_layers( + self, +) -> LayerCircuit: + """Merges measurement layers together, despite intervening annotation layers. + + For example, this method would turn this circuit fragment: + + M 0 + DETECTOR(0, 0) rec[-1] + OBSERVABLE_INCLUDE(5) rec[-1] + SHIFT_COORDS(0, 1) + M 1 + DETECTOR(0, 0) rec[-1] + + into this circuit fragment: + + M 0 1 + DETECTOR(0, 0) rec[-2] + OBSERVABLE_INCLUDE(5) rec[-2] + SHIFT_COORDS(0, 1) + DETECTOR(0, 0) rec[-1] + """ +``` + + +```python +# stimflow.LayerCircuit.with_locally_optimized_layers + +# (in class stimflow.LayerCircuit) +def with_locally_optimized_layers( + self, +) -> LayerCircuit: + """Iterates over the circuit aggregating layer.optimized(second_layer). + """ +``` + + +```python +# stimflow.LayerCircuit.with_qubit_coords_at_start + +# (in class stimflow.LayerCircuit) +def with_qubit_coords_at_start( + self, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.with_rotations_before_resets_removed + +# (in class stimflow.LayerCircuit) +def with_rotations_before_resets_removed( + self, + loop_boundary_resets: set[int] | None = None, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.with_rotations_merged_earlier + +# (in class stimflow.LayerCircuit) +def with_rotations_merged_earlier( + self, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.with_rotations_rolled_from_end_of_loop_to_start_of_loop + +# (in class stimflow.LayerCircuit) +def with_rotations_rolled_from_end_of_loop_to_start_of_loop( + self, +) -> LayerCircuit: + """Rewrites loops so that they only have rotations at the start, not the end. + + This is useful for ensuring loops don't redundantly rotate at the loop boundary, + by merging the rotations at the end with the rotations at the start or by + making it clear rotations at the end were not needed because of the + operations coming next. + + For example, this: + + REPEAT 5 { + S 2 3 4 + R 0 1 + ... + M 0 1 + H 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + DETECTOR rec[-1] + } + + will become this: + + REPEAT 5 { + H 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + S 2 3 4 + R 0 1 + ... + M 0 1 + DETECTOR rec[-1] + } + + which later optimization passes can then reduce further. + """ +``` + + +```python +# stimflow.LayerCircuit.with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer + +# (in class stimflow.LayerCircuit) +def with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer( + self, + layer_types: type | tuple[type, ...], +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type + +# (in class stimflow.LayerCircuit) +def with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type( + self, + layer_types: type | tuple[type, ...], +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier + +# (in class stimflow.LayerCircuit) +def with_whole_rotation_layers_slid_earlier( + self, +) -> LayerCircuit: +``` + + +```python +# stimflow.LayerCircuit.without_empty_layers + +# (in class stimflow.LayerCircuit) +def without_empty_layers( + self, +) -> LayerCircuit: + """Removes empty layers from the circuit. + + Empty layers are sometimes created as a byproduct of certain optimizations, or may have been + present in the original circuit. Usually they are unwanted, and this method removes them. + """ +``` + + +```python +# stimflow.LineData + +# (at top-level in the stimflow module) +class LineData: +``` + + +```python +# stimflow.LineData.__init__ + +# (in class stimflow.LineData) +def __init__( + self, + *, + rgba: tuple[float, float, float, float], + edge_list: np.ndarray, +): + """Lines with associated color information. + + Args: + rgba: Red, green, blue, and alpha data to associate with all the lines. + Each value should range from 0 to 1. + (The alpha data is ignored in most viewers, but needed by the 3d model format.) + edge_list: A 3d float32 numpy array with shape == (*, 2, 3). + Axis 0 is the triangle axis (each entry is a triangle). + Axis 1 is the AB vertex axis (each entry is a vertex from the edge). + Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). + """ +``` + + +```python +# stimflow.LineData.fused + +# (in class stimflow.LineData) +def fused( + data: Iterable[LineData], +) -> list[LineData]: + """Attempts to combine line data instances into fewer instances. + """ +``` + + +```python +# stimflow.MeasureLayer + +# (at top-level in the stimflow module) +@dataclasses.dataclass +class MeasureLayer: + """A layer of single qubit Pauli basis measurement operations. + """ + targets: list[int] + bases: list[str] +``` + + +```python +# stimflow.MeasureLayer.append_into_stim_circuit + +# (in class stimflow.MeasureLayer) +def append_into_stim_circuit( + self, + out: stim.Circuit, +) -> None: +``` + + +```python +# stimflow.MeasureLayer.copy + +# (in class stimflow.MeasureLayer) +def copy( + self, +) -> MeasureLayer: +``` + + +```python +# stimflow.MeasureLayer.locally_optimized + +# (in class stimflow.MeasureLayer) +def locally_optimized( + self, + next_layer: Layer | None, +) -> list[Layer | None]: +``` + + +```python +# stimflow.MeasureLayer.to_z_basis + +# (in class stimflow.MeasureLayer) +def to_z_basis( + self, +) -> list[Layer]: +``` + + +```python +# stimflow.MeasureLayer.touched + +# (in class stimflow.MeasureLayer) +def touched( + self, +) -> set[int]: +``` + + +```python +# stimflow.NoiseModel + +# (at top-level in the stimflow module) +class NoiseModel: + """Converts circuits into noisy circuits according to rules. + """ +``` + + +```python +# stimflow.NoiseModel.noisy_circuit + +# (in class stimflow.NoiseModel) +def noisy_circuit( + self, + circuit: stim.Circuit, + *, + system_qubit_indices: set[int] | None = None, + immune_qubit_indices: Iterable[int] | None = None, + immune_qubit_coords: Iterable[complex | float | int | Iterable[float | int]] | None = None, +) -> stim.Circuit: + """Returns a noisy version of the given circuit, by applying the receiving noise model. + + Args: + circuit: The circuit to layer noise over. + system_qubit_indices: All qubits used by the circuit. These are the qubits eligible for + idling noise. + immune_qubit_indices: Qubits to not apply noise to, even if they are operated on. + immune_qubit_coords: Qubit coordinates to not apply noise to, even if they are operated + on. + + Returns: + The noisy version of the circuit. + """ +``` + + +```python +# stimflow.NoiseModel.noisy_circuit_skipping_mpp_boundaries + +# (in class stimflow.NoiseModel) +def noisy_circuit_skipping_mpp_boundaries( + self, + circuit: stim.Circuit, + *, + immune_qubit_indices: Set[int] | None = None, + immune_qubit_coords: Iterable[complex | float | int | Iterable[float | int]] | None = None, +) -> stim.Circuit: + """Adds noise to the circuit except for MPP operations at the start/end. + + Divides the circuit into three parts: mpp_start, body, mpp_end. The mpp + sections grow from the ends of the circuit until they hit an instruction + that's not an annotation or an MPP. Then body is the remaining circuit + between the two ends. Noise is added to the body, and then the pieces + are reassembled. + """ +``` + + +```python +# stimflow.NoiseModel.si1000 + +# (in class stimflow.NoiseModel) +def si1000( + p: float, +) -> NoiseModel: + """Superconducting inspired noise. + + As defined in "A Fault-Tolerant Honeycomb Memory" https://arxiv.org/abs/2108.10457 + + Small tweak when measurements aren't immediately followed by a reset: the measurement result + is probabilistically flipped instead of the input qubit. The input qubit is depolarized + after the measurement. + """ +``` + + +```python +# stimflow.NoiseModel.uniform_depolarizing + +# (in class stimflow.NoiseModel) +def uniform_depolarizing( + p: float, + *, + single_qubit_only: bool = False, +) -> NoiseModel: + """Near-standard circuit depolarizing noise. + + Everything has the same parameter p. + Single qubit clifford gates get single qubit depolarization. + Two qubit clifford gates get single qubit depolarization. + Dissipative gates have their result probabilistically bit flipped (or phase flipped if + appropriate). + + Non-demolition measurement is treated a bit unusually in that it is the result that is + flipped instead of the input qubit. The input qubit is depolarized. + """ +``` + + +```python +# stimflow.NoiseRule + +# (at top-level in the stimflow module) +class NoiseRule: + """Describes how to add noise to an operation. + """ +``` + + +```python +# stimflow.NoiseRule.__init__ + +# (in class stimflow.NoiseRule) +def __init__( + self, + *, + before: dict[str, float | tuple[float, ...]] | None = None, + after: dict[str, float | tuple[float, ...]] | None = None, + flip_result: float = 0, +): + """ + + Args: + after: A dictionary mapping noise rule names to their probability argument. + For example, {"DEPOLARIZE2": 0.01, "X_ERROR": 0.02} will add two qubit + depolarization with parameter 0.01 and also add 2% bit flip noise. These + noise channels occur after all other operations in the moment and are applied + to the same targets as the relevant operation. + flip_result: The probability that a measurement result should be reported incorrectly. + Only valid when applied to operations that produce measurement results. + """ +``` + + +```python +# stimflow.NoiseRule.append_noisy_version_of + +# (in class stimflow.NoiseRule) +def append_noisy_version_of( + self, + *, + split_op: stim.CircuitInstruction, + out_during_moment: stim.Circuit, + before_moments: collections.defaultdict[Any, stim.Circuit], + after_moments: collections.defaultdict[Any, stim.Circuit], + immune_qubit_indices: Set[int], +) -> None: +``` + + +```python +# stimflow.Patch + +# (at top-level in the stimflow module) +class Patch: + """A collection of annotated stabilizers. + """ +``` + + +```python +# stimflow.Patch.data_set + +# (in class stimflow.Patch) +class data_set: + """Returns the set of all data qubits used by tiles in the patch. + """ +``` + + +```python +# stimflow.Patch.m2tile + +# (in class stimflow.Patch) +class m2tile: +``` + + +```python +# stimflow.Patch.measure_set + +# (in class stimflow.Patch) +class measure_set: + """Returns the set of all measure qubits used by tiles in the patch. + """ +``` + + +```python +# stimflow.Patch.partitioned_tiles + +# (in class stimflow.Patch) +class partitioned_tiles: + """Returns the tiles of the patch, but split into non-overlapping groups. + """ +``` + + +```python +# stimflow.Patch.to_svg + +# (in class stimflow.Patch) +def to_svg( + self, + *, + title: str | list[str] | None = None, + other: Patch | StabilizerCode | Iterable[Patch | StabilizerCode] = (), + show_order: bool = False, + show_measure_qubits: bool = False, + show_data_qubits: bool = True, + system_qubits: Iterable[complex] = (), + show_coords: bool = True, + opacity: float = 1, + show_obs: bool = False, + rows: int | None = None, + cols: int | None = None, + tile_color_func: Callable[[Tile], str] | None = None, +) -> str_svg: +``` + + +```python +# stimflow.Patch.used_set + +# (in class stimflow.Patch) +class used_set: + """Returns the set of all data and measure qubits used by tiles in the patch. + """ +``` + + +```python +# stimflow.Patch.with_edits + +# (in class stimflow.Patch) +def with_edits( + self, + *, + tiles: Iterable[Tile] | None = None, +) -> Patch: +``` + + +```python +# stimflow.Patch.with_only_x_tiles + +# (in class stimflow.Patch) +def with_only_x_tiles( + self, +) -> Patch: +``` + + +```python +# stimflow.Patch.with_only_y_tiles + +# (in class stimflow.Patch) +def with_only_y_tiles( + self, +) -> Patch: +``` + + +```python +# stimflow.Patch.with_only_z_tiles + +# (in class stimflow.Patch) +def with_only_z_tiles( + self, +) -> Patch: +``` + + +```python +# stimflow.Patch.with_remaining_degrees_of_freedom_as_logicals + +# (in class stimflow.Patch) +def with_remaining_degrees_of_freedom_as_logicals( + self, +) -> StabilizerCode: + """Solves for the logical observables, given only the stabilizers. + """ +``` + + +```python +# stimflow.Patch.with_transformed_bases + +# (in class stimflow.Patch) +def with_transformed_bases( + self, + basis_transform: "Callable[[Literal['X, 'Y, 'Z']], Literal['X, 'Y, 'Z']]", +) -> Patch: +``` + + +```python +# stimflow.Patch.with_transformed_coords + +# (in class stimflow.Patch) +def with_transformed_coords( + self, + coord_transform: Callable[[complex], complex], +) -> Patch: +``` + + +```python +# stimflow.Patch.with_xz_flipped + +# (in class stimflow.Patch) +def with_xz_flipped( + self, +) -> Patch: +``` + + +```python +# stimflow.PauliMap + +# (at top-level in the stimflow module) +class PauliMap: + """A qubit-to-pauli mapping. + """ +``` + + +```python +# stimflow.PauliMap.__init__ + +# (in class stimflow.PauliMap) +def __init__( + self, + mapping: "dict[complex, Literal['X, 'Y, 'Z'] | str] | dict[Literal['X, 'Y, 'Z'] | str, complex | Iterable[complex]] | PauliMap | Tile | stim.PauliString | None" = None, + *, + name: Any = None, +): + """Initializes a PauliMap using maps of Paulis to/from qubits. + + Args: + mapping: The association between qubits and paulis, specifiable in a variety of ways. + name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, + in order to identify the Pauli map. A common convention used in the library is that + named Pauli maps correspond to logical operators. + """ +``` + + +```python +# stimflow.PauliMap.anticommutes + +# (in class stimflow.PauliMap) +def anticommutes( + self, + other: PauliMap, +) -> bool: + """Determines if the pauli map anticommutes with another pauli map. + """ +``` + + +```python +# stimflow.PauliMap.commutes + +# (in class stimflow.PauliMap) +def commutes( + self, + other: PauliMap, +) -> bool: + """Determines if the pauli map commutes with another pauli map. + """ +``` + + +```python +# stimflow.PauliMap.from_xs + +# (in class stimflow.PauliMap) +def from_xs( + xs: Iterable[complex], + *, + name: Any = None, +) -> PauliMap: + """Returns a PauliMap mapping the given qubits to the X basis. + """ +``` + + +```python +# stimflow.PauliMap.from_ys + +# (in class stimflow.PauliMap) +def from_ys( + ys: Iterable[complex], + *, + name: Any = None, +) -> PauliMap: + """Returns a PauliMap mapping the given qubits to the Y basis. + """ +``` + + +```python +# stimflow.PauliMap.from_zs + +# (in class stimflow.PauliMap) +def from_zs( + zs: Iterable[complex], + *, + name: Any = None, +) -> PauliMap: + """Returns a PauliMap mapping the given qubits to the Z basis. + """ +``` + + +```python +# stimflow.PauliMap.get + +# (in class stimflow.PauliMap) +def get( + self, + key: complex, + default: Any = None, +) -> Any: +``` + + +```python +# stimflow.PauliMap.items + +# (in class stimflow.PauliMap) +def items( + self, +) -> "Iterable[tuple[complex, Literal['X', 'Y', 'Z']]]": + """Returns the (qubit, basis) pairs of the PauliMap. + """ +``` + + +```python +# stimflow.PauliMap.keys + +# (in class stimflow.PauliMap) +def keys( + self, +) -> Set[complex]: + """Returns the qubits of the PauliMap. + """ +``` + + +```python +# stimflow.PauliMap.to_stim_pauli_string + +# (in class stimflow.PauliMap) +def to_stim_pauli_string( + self, + q2i: dict[complex, int], + *, + num_qubits: int | None = None, +) -> stim.PauliString: + """Converts into a stim.PauliString. + """ +``` + + +```python +# stimflow.PauliMap.to_stim_targets + +# (in class stimflow.PauliMap) +def to_stim_targets( + self, + q2i: dict[complex, int], +) -> list[stim.GateTarget]: + """Converts into a stim combined pauli target like 'X1*Y2*Z3'. + """ +``` + + +```python +# stimflow.PauliMap.to_tile + +# (in class stimflow.PauliMap) +def to_tile( + self, +) -> Tile: + """Converts the PauliMap into a stimflow.Tile. + """ +``` + + +```python +# stimflow.PauliMap.values + +# (in class stimflow.PauliMap) +def values( + self, +) -> "Iterable[Literal['X', 'Y', 'Z']]": + """Returns the bases used by the PauliMap. + """ +``` + + +```python +# stimflow.PauliMap.with_basis + +# (in class stimflow.PauliMap) +def with_basis( + self, + basis: "Literal['X, 'Y, 'Z']", +) -> PauliMap: + """Returns the same PauliMap, but with all its qubits mapped to the given basis. + """ +``` + + +```python +# stimflow.PauliMap.with_name + +# (in class stimflow.PauliMap) +def with_name( + self, + name: Any, +) -> PauliMap: + """Returns the same PauliMap, but with the given name. + + Names are used to identify logical operators. + """ +``` + + +```python +# stimflow.PauliMap.with_transformed_coords + +# (in class stimflow.PauliMap) +def with_transformed_coords( + self, + transform: Callable[[complex], complex], +) -> PauliMap: + """Returns the same PauliMap but with coordinates transformed by the given function. + """ +``` + + +```python +# stimflow.PauliMap.with_xy_flipped + +# (in class stimflow.PauliMap) +def with_xy_flipped( + self, +) -> PauliMap: + """Returns the same PauliMap, but with all qubits conjugated by H_XY. + """ +``` + + +```python +# stimflow.PauliMap.with_xz_flipped + +# (in class stimflow.PauliMap) +def with_xz_flipped( + self, +) -> PauliMap: + """Returns the same PauliMap, but with all qubits conjugated by H. + """ +``` + + +```python +# stimflow.ResetLayer + +# (at top-level in the stimflow module) +@dataclasses.dataclass +class ResetLayer: + """A layer of reset gates. + """ + targets: dict[int, Literal['X', 'Y', 'Z']] +``` + + +```python +# stimflow.ResetLayer.append_into_stim_circuit + +# (in class stimflow.ResetLayer) +def append_into_stim_circuit( + self, + out: stim.Circuit, +) -> None: +``` + + +```python +# stimflow.ResetLayer.copy + +# (in class stimflow.ResetLayer) +def copy( + self, +) -> ResetLayer: +``` + + +```python +# stimflow.ResetLayer.locally_optimized + +# (in class stimflow.ResetLayer) +def locally_optimized( + self, + next_layer: Layer | None, +) -> list[Layer | None]: +``` + + +```python +# stimflow.ResetLayer.to_z_basis + +# (in class stimflow.ResetLayer) +def to_z_basis( + self, +) -> list[Layer]: +``` + + +```python +# stimflow.ResetLayer.touched + +# (in class stimflow.ResetLayer) +def touched( + self, +) -> set[int]: +``` + + +```python +# stimflow.RotationLayer + +# (at top-level in the stimflow module) +@dataclasses.dataclass +class RotationLayer: + """A layer of single qubit Clifford rotation gates. + """ + named_rotations: dict[int, str] +``` + + +```python +# stimflow.RotationLayer.append_into_stim_circuit + +# (in class stimflow.RotationLayer) +def append_into_stim_circuit( + self, + out: stim.Circuit, +) -> None: +``` + + +```python +# stimflow.RotationLayer.append_named_rotation + +# (in class stimflow.RotationLayer) +def append_named_rotation( + self, + name: str, + target: int, +): +``` + + +```python +# stimflow.RotationLayer.copy + +# (in class stimflow.RotationLayer) +def copy( + self, +) -> RotationLayer: +``` + + +```python +# stimflow.RotationLayer.inverse + +# (in class stimflow.RotationLayer) +def inverse( + self, +) -> RotationLayer: +``` + + +```python +# stimflow.RotationLayer.is_vacuous + +# (in class stimflow.RotationLayer) +def is_vacuous( + self, +) -> bool: +``` + + +```python +# stimflow.RotationLayer.locally_optimized + +# (in class stimflow.RotationLayer) +def locally_optimized( + self, + next_layer: Layer | None, +) -> list[Layer | None]: +``` + + +```python +# stimflow.RotationLayer.prepend_named_rotation + +# (in class stimflow.RotationLayer) +def prepend_named_rotation( + self, + name: str, + target: int, +): +``` + + +```python +# stimflow.RotationLayer.touched + +# (in class stimflow.RotationLayer) +def touched( + self, +) -> set[int]: +``` + + +```python +# stimflow.StabilizerCode + +# (at top-level in the stimflow module) +class StabilizerCode: + """This class stores the stabilizers and logicals of a stabilizer code. + + The exact semantics of the class are somewhat loose. For example, by default + this class doesn't verify that its fields actually form a valid stabilizer + code. This is so that the class can be used as a sort of useful data dumping + ground even in cases where what is being built isn't a stabilizer code. For + example, you can store a gauge code in the fields... it's just that methods + like 'make_code_capacity_circuit' will no longer work. + + The stabilizers are stored as a `stimflow.Patch`. A patch is like a list of `stimflow.PauliMap`, + except it actually stores `stimflow.Tile` instances so additional annotations can be added + and additional utility methods are easily available. + """ +``` + + +```python +# stimflow.StabilizerCode.__init__ + +# (in class stimflow.StabilizerCode) +def __init__( + self, + stabilizers: Iterable[Tile | PauliMap] | Patch | None = None, + *, + logicals: Iterable[PauliMap | tuple[PauliMap, PauliMap]] = (), + scattered_logicals: Iterable[PauliMap] = (), +): + """ + + Args: + stabilizers: The stabilizers of the code, specified as a Patch + logicals: The logical qubits and/or observables of the code. Each entry should be + either a pair of anti-commuting stimflow.PauliMap (e.g. the X and Z observables of the + logical qubit) or a single stimflow.PauliMap (e.g. just the X observable). + scattered_logicals: Logical operators with arbitrary commutation relationships to each + other. Still expected to commute with the stabilizers. + """ +``` + + +```python +# stimflow.StabilizerCode.as_interface + +# (in class stimflow.StabilizerCode) +def as_interface( + self, +) -> stimflow.ChunkInterface: +``` + + +```python +# stimflow.StabilizerCode.concat_over + +# (in class stimflow.StabilizerCode) +def concat_over( + self, + under: StabilizerCode, + *, + skip_inner_stabilizers: bool = False, +) -> StabilizerCode: + """Computes the interleaved concatenation of two stabilizer codes. + """ +``` + + +```python +# stimflow.StabilizerCode.data_set + +# (in class stimflow.StabilizerCode) +class data_set: +``` + + +```python +# stimflow.StabilizerCode.find_distance + +# (in class stimflow.StabilizerCode) +def find_distance( + self, + *, + max_search_weight: int, +) -> int: +``` + + +```python +# stimflow.StabilizerCode.find_logical_error + +# (in class stimflow.StabilizerCode) +def find_logical_error( + self, + *, + max_search_weight: int, +) -> list[stim.ExplainedError]: +``` + + +```python +# stimflow.StabilizerCode.flat_logicals + +# (in class stimflow.StabilizerCode) +class flat_logicals: + """Returns a list of the logical operators defined by the stabilizer code. + + It's "flat" because paired X/Z logicals are returned separately instead of + as a tuple. + """ +``` + + +```python +# stimflow.StabilizerCode.from_patch_with_inferred_observables + +# (in class stimflow.StabilizerCode) +def from_patch_with_inferred_observables( + patch: Patch, +) -> StabilizerCode: +``` + + +```python +# stimflow.StabilizerCode.get_observable_by_basis + +# (in class stimflow.StabilizerCode) +def get_observable_by_basis( + self, + index: int, + basis: "Literal['X, 'Y, 'Z'] | str", + *, + default: Any = '__!not_specified, +) -> PauliMap: +``` + + +```python +# stimflow.StabilizerCode.list_pure_basis_observables + +# (in class stimflow.StabilizerCode) +def list_pure_basis_observables( + self, + basis: "Literal['X, 'Y, 'Z']", +) -> list[PauliMap]: +``` + + +```python +# stimflow.StabilizerCode.make_code_capacity_circuit + +# (in class stimflow.StabilizerCode) +def make_code_capacity_circuit( + self, + *, + noise: float | NoiseRule, + metadata_func: Callable[[stimflow.Flow], stimflow.FlowMetadata] = lambda _: FlowMetadata(), +) -> stim.Circuit: + """Produces a code capacity noisy memory experiment circuit for the stabilizer code. + """ +``` + + +```python +# stimflow.StabilizerCode.make_phenom_circuit + +# (in class stimflow.StabilizerCode) +def make_phenom_circuit( + self, + *, + noise: float | NoiseRule, + rounds: int, + metadata_func: Callable[[stimflow.Flow], stimflow.FlowMetadata] = lambda _: FlowMetadata(), +) -> stim.Circuit: + """Produces a phenomenological noise memory experiment circuit for the stabilizer code. + """ +``` + + +```python +# stimflow.StabilizerCode.measure_set + +# (in class stimflow.StabilizerCode) +class measure_set: +``` + + +```python +# stimflow.StabilizerCode.patch + +# (in class stimflow.StabilizerCode) +@property +def patch( + self, +): + """Returns the stimflow.Patch storing the stabilizers of the code. + """ +``` + + +```python +# stimflow.StabilizerCode.physical_to_logical + +# (in class stimflow.StabilizerCode) +def physical_to_logical( + self, + ps: stim.PauliString, +) -> PauliMap: + """Maps a physical qubit string into a logical qubit string. + + Requires that all logicals be specified as X/Z tuples. + """ +``` + + +```python +# stimflow.StabilizerCode.tiles + +# (in class stimflow.StabilizerCode) +@property +def tiles( + self, +): + """Returns the tiles of the code's stabilizer patch. + """ +``` + + +```python +# stimflow.StabilizerCode.to_svg + +# (in class stimflow.StabilizerCode) +def to_svg( + self, + *, + title: str | list[str] | None = None, + canvas_height: int | None = None, + show_order: bool = False, + show_measure_qubits: bool = False, + show_data_qubits: bool = True, + system_qubits: Iterable[complex] = (), + opacity: float = 1, + show_coords: bool = True, + show_obs: bool = True, + other: stimflow.StabilizerCode | Patch | Iterable[stimflow.StabilizerCode | Patch] | None = None, + tile_color_func: Callable[[stimflow.Tile], str | tuple[float, float, float] | tuple[float, float, float, float] | None] | None = None, + rows: int | None = None, + cols: int | None = None, + find_logical_err_max_weight: int | None = None, + stabilizer_style: "Literal['polygon, 'circles'] | None" = 'polygon, + observable_style: "Literal['label, 'polygon, 'circles']" = 'label, +) -> str_svg: + """Returns an SVG diagram of the stabilizer code. + """ +``` + + +```python +# stimflow.StabilizerCode.transversal_init_chunk + +# (in class stimflow.StabilizerCode) +def transversal_init_chunk( + self, + *, + basis: "Literal['X, 'Y, 'Z'] | str | stimflow.PauliMap | dict[complex, str | Literal['X, 'Y, 'Z']]", +) -> stimflow.Chunk: + """Returns a chunk that describes initializing the stabilizer code with given reset bases. + + Stabilizers that anticommute with the resets will be discarded flows. + + The returned chunk isn't guaranteed to be fault tolerant. + """ +``` + + +```python +# stimflow.StabilizerCode.transversal_measure_chunk + +# (in class stimflow.StabilizerCode) +def transversal_measure_chunk( + self, + *, + basis: "Literal['X, 'Y, 'Z'] | str | stimflow.PauliMap | dict[complex, str | Literal['X, 'Y, 'Z']]", +) -> stimflow.Chunk: + """Returns a chunk that describes measuring the stabilizer code with given measure bases. + + Stabilizers that anticommute with the measurements will be discarded flows. + + The returned chunk isn't guaranteed to be fault tolerant. + """ +``` + + +```python +# stimflow.StabilizerCode.used_set + +# (in class stimflow.StabilizerCode) +class used_set: +``` + + +```python +# stimflow.StabilizerCode.verify + +# (in class stimflow.StabilizerCode) +def verify( + self, +) -> None: + """Verifies observables and stabilizers relate as a stabilizer code. + + All stabilizers should commute with each other. + All stabilizers should commute with all observables. + Same-index X and Z observables should anti-commute. + All other observable pairs should commute. + """ +``` + + +```python +# stimflow.StabilizerCode.verify_distance_is_at_least_2 + +# (in class stimflow.StabilizerCode) +def verify_distance_is_at_least_2( + self, +): + """Verifies undetected logical errors require at least 2 physical errors. + + Verifies using a code capacity noise model. + """ +``` + + +```python +# stimflow.StabilizerCode.verify_distance_is_at_least_3 + +# (in class stimflow.StabilizerCode) +def verify_distance_is_at_least_3( + self, +): + """Verifies undetected logical errors require at least 3 physical errors. + + Verifies using a code capacity noise model. + """ +``` + + +```python +# stimflow.StabilizerCode.with_edits + +# (in class stimflow.StabilizerCode) +def with_edits( + self, + *, + stabilizers: Iterable[Tile | PauliMap] | Patch | None = None, + logicals: Iterable[PauliMap | tuple[PauliMap, PauliMap]] | None = None, +) -> StabilizerCode: +``` + + +```python +# stimflow.StabilizerCode.with_integer_coordinates + +# (in class stimflow.StabilizerCode) +def with_integer_coordinates( + self, +) -> StabilizerCode: + """Returns an equivalent stabilizer code, but with all qubit on Gaussian integers. + """ +``` + + +```python +# stimflow.StabilizerCode.with_observables_from_basis + +# (in class stimflow.StabilizerCode) +def with_observables_from_basis( + self, + basis: "Literal['X, 'Y, 'Z']", +) -> StabilizerCode: +``` + + +```python +# stimflow.StabilizerCode.with_remaining_degrees_of_freedom_as_logicals + +# (in class stimflow.StabilizerCode) +def with_remaining_degrees_of_freedom_as_logicals( + self, +) -> StabilizerCode: + """Solves for the logical observables, given only the stabilizers. + """ +``` + + +```python +# stimflow.StabilizerCode.with_transformed_coords + +# (in class stimflow.StabilizerCode) +def with_transformed_coords( + self, + coord_transform: Callable[[complex], complex], +) -> StabilizerCode: + """Returns the same stabilizer code, but with coordinates transformed by the given + function. + """ +``` + + +```python +# stimflow.StabilizerCode.with_xz_flipped + +# (in class stimflow.StabilizerCode) +def with_xz_flipped( + self, +) -> StabilizerCode: + """Returns the same stabilizer code, but with all qubits Hadamard conjugated. + """ +``` + + +```python +# stimflow.StabilizerCode.x_basis_subset + +# (in class stimflow.StabilizerCode) +def x_basis_subset( + self, +) -> StabilizerCode: +``` + + +```python +# stimflow.StabilizerCode.z_basis_subset + +# (in class stimflow.StabilizerCode) +def z_basis_subset( + self, +) -> StabilizerCode: +``` + + +```python +# stimflow.StimCircuitLoom + +# (at top-level in the stimflow module) +class StimCircuitLoom: + """Class for combining stim circuits running in parallel at separate locations. + + for standard usage, call StimCircuitLoom.weave(...), which returns the weaved circuit + for usage details, see the docstring to that function + + for complex usage, you can instantiate a loom StimCircuitLoom(...) + This is lets you access details of the weaving afterward, such as the measurement mapping + """ +``` + + +```python +# stimflow.StimCircuitLoom.weave + +# (in class stimflow.StimCircuitLoom) +class weave: + """Combines two stim circuits instruction by instruction. + + Example usage: + StimCircuitLoom.weave(circuit_0, circuit_1) -> stim.Circuit + + Expects that the input circuit have 'matching instructions', in that they + contain exactly the same sequence of instructions which can be matched up + 1-to-1. This may require one circuit to have instructions with no targets, + purely to match instructions in the other circuit. Exceptions to this are + the annotation instructions DETECTOR, OBSERVABLE_INCLUDE, QUBIT_COORDS, + and SHIFT_COORDS, which do not need a matching statement in the other + circuit. This may not be what you want, as it will produce duplicate + DETECTOR or QUBIT_COORD instructions if they are included in both circuits. + The annotation TICK is considered a matching instruction. + + Generally, instructions are combined by placing all targets from the + first circuit instruction, followed by all targets from the second. + + In most gates, if a gate target is present in the first instruction + target list, it is removed from the second instructions target list. + As such, we do not permit instructions in the input circuits to have + duplicate targets. This avoids the ambiguity of deciding whether one + or both duplicates between circuits have to match up. + + Measure record targets are adjusted to point to the correct record in the + combined circuit e.g. DETECTOR rec[-1] or CX rec[-1] 1 + + Sweep bits are not handled by default, and will produce a ValueError. + If sweep_bit_func is provided, it will be used to produce new sweep bit + targets as follows: + new_sweep_bit_index = sweep_bit_func(circuit_index, sweep_bit_index) + where: + circuit_index = 0 for circuit_0 and 1 for circuit_1 + sweep_bit_index is the sweep bit index used in the input circuit + """ +``` + + +```python +# stimflow.StimCircuitLoom.weaved_target_rec_from_c0 + +# (in class stimflow.StimCircuitLoom) +def weaved_target_rec_from_c0( + self, + target_rec: int, +) -> int: + """given a target rec in circuit_0, return the equiv rec in the weaved circuit. + + args: + target_rec: a valid measurement record target in the input circuit + follows python indexing semantics: + can be either positive (counting from the start of the circuit, 0 indexed) + or negative (counting from the end backwards, last measurement is [-1]) + The second is compatible with stim instruction target rec values + + returns: + The same measurements target rec in the weaved circuit. + Always returns a negative 'lookback' compatible with a stim circuit + Add StimCircuitWeave.circuit.num_measurements for an absolute measurement index + """ +``` + + +```python +# stimflow.StimCircuitLoom.weaved_target_rec_from_c1 + +# (in class stimflow.StimCircuitLoom) +def weaved_target_rec_from_c1( + self, + target_rec: int, +) -> int: + """given a target rec in circuit_1, return the equiv rec in the weaved circuit. + """ +``` + + +```python +# stimflow.TextData + +# (at top-level in the stimflow module) +class TextData: +``` + + +```python +# stimflow.TextData.__init__ + +# (in class stimflow.TextData) +def __init__( + self, + *, + text: str, + start: Sequence[float], + forward: Sequence[float], + up: Sequence[float], + mirror_backside: bool = True, +): + """Describes a rectangle showing text. + + Args: + text: The text to draw in the rectangle. + start: The 3d point where the rectangle and text starts. + This is the `bottom_left` of the rectangle, in 3d. + forward: The 3d direction along which the text grows as the message gets longer. + This is the `bottom_right - bottom_left` of the rectangle, in 3d. + The length of this vector is ignored. + up: A 3d direction along which the text is oriented. + This is the `top_left - bottom_left` of the rectangle, in 3d. + The length of this vector is ignored. + Should be perpendicular to `forward`. + mirror_backside: Determines whether or not the text on the back of the rectangle + is mirrored (making it readable) or not (keeping the forward direction consistent). + Defaults to True (readable on both sides). + """ +``` + + +```python +# stimflow.Tile + +# (at top-level in the stimflow module) +class Tile: + """A stabilizer with some associated metadata. + + The exact meaning of the tile's fields are often context dependent. For example, + different circuits will use the measure qubit in different ways (or not at all) + and the flags could be essentially anything at all. Tile is intended to be useful + as an intermediate step in the production of a circuit. + + For example, it's much easier to create a color code circuit when you have a list + of the hexagonal and trapezoidal shapes making up the color code. So it's natural to + split the color code circuit generation problem into two steps: (1) making the shapes + then (2) making the circuit given the shapes. In other words, deal with the spatial + complexities first then deal with the temporal complexities second. The Tile class + is a reasonable representation for the shapes, because: + + - The X/Z basis of the stabilizer can be stored in the `bases` field. + - The red/green/blue coloring can be stored as flags. + - The ancilla qubits for the shapes be stored as measure_qubit values. + - You can get diagrams of the shapes by passing the tiles into a `stimflow.Patch`. + - You can verify the tiles form a code by passing the patch into a `stimflow.StabilizerCode`. + """ +``` + + +```python +# stimflow.Tile.__init__ + +# (in class stimflow.Tile) +def __init__( + self, + *, + bases: str, + data_qubits: Iterable[complex | None], + measure_qubit: complex | None = None, + flags: Iterable[str] = (), +): + """ + + Args: + bases: Basis of the stabilizer. A string of XYZ characters the same + length as the data_qubits argument. It is permitted to + give a single-character string, which will automatically be + expanded to the full length. For example, 'X' will become 'XXXX' + if there are four data qubits. + measure_qubit: The ancilla qubit used to measure the stabilizer. + data_qubits: The data qubits in the stabilizer, in the order + that they are interacted with. Some entries may be None, + indicating that no data qubit is interacted with during the + corresponding interaction layer. + """ +``` + + +```python +# stimflow.Tile.basis + +# (in class stimflow.Tile) +class basis: +``` + + +```python +# stimflow.Tile.center + +# (in class stimflow.Tile) +def center( + self, +) -> complex: +``` + + +```python +# stimflow.Tile.data_set + +# (in class stimflow.Tile) +class data_set: +``` + + +```python +# stimflow.Tile.to_pauli_map + +# (in class stimflow.Tile) +def to_pauli_map( + self, +) -> PauliMap: +``` + + +```python +# stimflow.Tile.used_set + +# (in class stimflow.Tile) +class used_set: +``` + + +```python +# stimflow.Tile.with_bases + +# (in class stimflow.Tile) +def with_bases( + self, + bases: str, +) -> Tile: +``` + + +```python +# stimflow.Tile.with_basis + +# (in class stimflow.Tile) +def with_basis( + self, + bases: str, +) -> Tile: +``` + + +```python +# stimflow.Tile.with_data_qubit_cleared + +# (in class stimflow.Tile) +def with_data_qubit_cleared( + self, + q: complex, +) -> Tile: +``` + + +```python +# stimflow.Tile.with_edits + +# (in class stimflow.Tile) +def with_edits( + self, + *, + bases: str | None = None, + measure_qubit: "complex | None | Literal['unspecified']" = 'unspecified, + data_qubits: Iterable[complex | None] | None = None, + flags: Iterable[str] | None = None, +) -> Tile: +``` + + +```python +# stimflow.Tile.with_transformed_bases + +# (in class stimflow.Tile) +def with_transformed_bases( + self, + basis_transform: "Callable[[Literal['X, 'Y, 'Z']], Literal['X, 'Y, 'Z']]", +) -> Tile: +``` + + +```python +# stimflow.Tile.with_transformed_coords + +# (in class stimflow.Tile) +def with_transformed_coords( + self, + coord_transform: Callable[[complex], complex], +) -> Tile: +``` + + +```python +# stimflow.Tile.with_xz_flipped + +# (in class stimflow.Tile) +def with_xz_flipped( + self, +) -> Tile: +``` + + +```python +# stimflow.TriangleData + +# (at top-level in the stimflow module) +class TriangleData: +``` + + +```python +# stimflow.TriangleData.__init__ + +# (in class stimflow.TriangleData) +def __init__( + self, + *, + rgba: tuple[float, float, float, float], + triangle_list: np.ndarray, +): + """Triangles with associated color information. + + Args: + rgba: Red, green, blue, and alpha data to associate with all the triangles. + Each value should range from 0 to 1. + (The alpha data is ignored in most viewers, but needed by the 3d model format.) + triangle_list: A 3d float32 numpy array with shape == (*, 3, 3). + Axis 0 is the triangle axis (each entry is a triangle). + Axis 1 is the ABC vertex axis (each entry is a vertex from the triangle). + Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). + """ +``` + + +```python +# stimflow.TriangleData.fused + +# (in class stimflow.TriangleData) +def fused( + data: Iterable[TriangleData], +) -> list[TriangleData]: + """Attempts to combine triangle data instances into fewer instances. + """ +``` + + +```python +# stimflow.TriangleData.rect + +# (in class stimflow.TriangleData) +def rect( + *, + rgba: tuple[float, float, float, float], + origin: Iterable[float], + d1: Iterable[float], + d2: Iterable[float], +) -> TriangleData: + """Creates a pair of triangles forming a rectangle. + + Args: + rgba: Color of the rectangle. + origin: Bottom-left corner of the rectangle. + d1: The right - left displacement. + d2: The top - bottom displacement. + """ +``` + + +```python +# stimflow.Viewable3dModelGLTF + +# (at top-level in the stimflow module) +@dataclasses.dataclass +class Viewable3dModelGLTF: + """A pygltflib.GLTF2 augmented with the ability to create a simple 3d viewer for the model. + """ + extensions: Optional[Dict[str, Any]] + extras: Optional[Dict[str, Any]] + accessors: List[pygltflib.Accessor] + animations: List[pygltflib.Animation] + asset: + bufferViews: List[pygltflib.BufferView] + buffers: List[pygltflib.Buffer] + cameras: List[pygltflib.Camera] + extensionsUsed: List[str] + extensionsRequired: List[str] + images: List[pygltflib.Image] + materials: List[pygltflib.Material] + meshes: List[pygltflib.Mesh] + nodes: List[pygltflib.Node] + samplers: List[pygltflib.Sampler] + scene: = None + scenes: List[pygltflib.Scene] + skins: List[pygltflib.Skin] + textures: List[pygltflib.Texture] +``` + + +```python +# stimflow.Viewable3dModelGLTF.html_viewer + +# (in class stimflow.Viewable3dModelGLTF) +def html_viewer( + self, +) -> str_html: + """Returns an HTML document that embeds the 3d model within a 3d viewer. + """ +``` + + +```python +# stimflow.append_reindexed_content_to_circuit + +# (at top-level in the stimflow module) +def append_reindexed_content_to_circuit( + *, + out_circuit: stim.Circuit, + content: stim.Circuit, + qubit_i2i: dict[int, int], + obs_i2i: "dict[int, int | Literal['discard']]", + rewrite_detector_time_coordinates: bool = False, +) -> None: + """Reindexes content and appends it to a circuit. + + Note that QUBIT_COORDS instructions are skipped. + + Args: + out_circuit: The output circuit. The circuit being edited. + content: The circuit to be appended to the output circuit. + qubit_i2i: A dictionary specifying how qubit indices are remapped. Indices outside the + map are not changed. + obs_i2i: A dictionary specifying how observable indices are remapped. Indices outside the + map are not changed. + rewrite_detector_time_coordinates: Defaults to False. When set to True, SHIFT_COORD and + DETECTOR instructions are automatically rewritten to track the passage of time without + using the same detector position twice at the same time. + """ +``` + + +```python +# stimflow.circuit_to_cycle_code_slices + +# (at top-level in the stimflow module) +def circuit_to_cycle_code_slices( + circuit: stim.Circuit, +) -> dict[int, StabilizerCode]: +``` + + +```python +# stimflow.circuit_to_dem_target_measurement_records_map + +# (at top-level in the stimflow module) +def circuit_to_dem_target_measurement_records_map( + circuit: stim.Circuit, +) -> dict[stim.DemTarget, list[int]]: +``` + + +```python +# stimflow.circuit_with_xz_flipped + +# (at top-level in the stimflow module) +def circuit_with_xz_flipped( + circuit: stim.Circuit, +) -> stim.Circuit: +``` + + +```python +# stimflow.compile_chunks_into_circuit + +# (at top-level in the stimflow module) +def compile_chunks_into_circuit( + chunks: list[Chunk | ChunkLoop | ChunkReflow], + *, + use_magic_time_boundaries: bool = False, + metadata_func: Callable[[Flow], FlowMetadata] = lambda _: FlowMetadata(), +) -> stim.Circuit: + """Stitches together a series of chunks into a fault tolerant circuit. + + Args: + chunks: The sequence of chunks to compile into a circuit. + use_magic_time_boundaries: Defaults to False. When False, an error will be raised if the + first chunk has any non-empty input flows or the last chunk has any non-empty output + flows (indicating the circuit is not complete). When True, the compiler will + automatically close those flows by inserting MPP and OBSERVABLE_INCLUDE instructions to + explain the dangling flows. + metadata_func: Defaults to using no metadata. This function should take a stimflow.Flow and + return a stimflow.FlowMetadata. The metadata is used for adding tags to coordinates to + DETECTOR instructions and tags to DETECTOR/OBSERVABLE_INCLUDE instructions. + + Returns: + The compiled circuit. + """ +``` + + +```python +# stimflow.complex_key + +# (at top-level in the stimflow module) +def complex_key( + c: complex, +) -> Any: +``` + + +```python +# stimflow.count_measurement_layers + +# (at top-level in the stimflow module) +def count_measurement_layers( + circuit: stim.Circuit, +) -> int: +``` + + +```python +# stimflow.find_d1_error + +# (at top-level in the stimflow module) +def find_d1_error( + obj: stim.Circuit | stim.DetectorErrorModel, +) -> stim.ExplainedError | stim.DemInstruction | None: +``` + + +```python +# stimflow.find_d2_error + +# (at top-level in the stimflow module) +def find_d2_error( + obj: stim.Circuit | stim.DetectorErrorModel, +) -> list[stim.ExplainedError] | stim.DetectorErrorModel | None: +``` + + +```python +# stimflow.gate_counts_for_circuit + +# (at top-level in the stimflow module) +def gate_counts_for_circuit( + circuit: stim.Circuit, +) -> collections.Counter[str]: + """Determines gates used by a circuit, disambiguating MPP/feedback cases. + + MPP instructions are expanded into what they actually measure, such as + "MXX" for MPP X1*X2 and "MXYZ" for MPP X4*Y5*Z7. + + Feedback instructions like `CX rec[-1] 0` become the gate "feedback". + + Sweep instructions like `CX sweep[2] 0` become the gate "sweep". + """ +``` + + +```python +# stimflow.gates_used_by_circuit + +# (at top-level in the stimflow module) +def gates_used_by_circuit( + circuit: stim.Circuit, +) -> set[str]: + """Determines gates used by a circuit, disambiguating MPP/feedback cases. + + MPP instructions are expanded into what they actually measure, such as + "MXX" for MPP X1*X2 and "MXYZ" for MPP X4*Y5*Z7. + + Feedback instructions like `CX rec[-1] 0` become the gate "feedback". + + Sweep instructions like `CX sweep[2] 0` become the gate "sweep". + """ +``` + + +```python +# stimflow.html_viewer_for_gltf_model + +# (at top-level in the stimflow module) +def html_viewer_for_gltf_model( + model: pygltflib.GLTF2, +) -> str_html: +``` + + +```python +# stimflow.make_3d_model + +# (at top-level in the stimflow module) +def make_3d_model( + elements: Iterable[TriangleData | LineData | TextData], +) -> Viewable3dModelGLTF: + """Creates a 3d model containing the elements. + + Args: + elements: A list of objects to include in the model. The list can include triangles + (TriangleData), lines (LineData), and text (TextData). + + Returns: + The 3d model, as a `stimflow.gltf_model`. + + `stimflow.gltf_model` inherits from `pygltflib.GLTF2` but adds a `_repr_html_` class + (creating a 3d viewer in Jupyter notebooks) and a `write_viewer_to` method for + saving a standalone HTML viewer. + """ +``` + + +```python +# stimflow.min_max_complex + +# (at top-level in the stimflow module) +def min_max_complex( + coords: Iterable[complex], + *, + default: complex | None = None, +) -> tuple[complex, complex]: + """Computes the bounding box of a collection of complex numbers. + + Args: + coords: The complex numbers to place a bounding box around. + default: If no elements are included, the bounding box will cover this + single value when the collection of complex numbers is empty. If + this argument isn't set (or is set to None), an exception will be + raised instead when given an empty collection. + + Returns: + A pair of complex values (c_min, c_max) where c_min's real component + where c_min is the minimum corner of the bounding box and c_max is the + maximum corner of the bounding box. + """ +``` + + +```python +# stimflow.sorted_complex + +# (at top-level in the stimflow module) +def sorted_complex( + values: Iterable[complex], +) -> list[complex]: +``` + + +```python +# stimflow.stim_circuit_html_viewer + +# (at top-level in the stimflow module) +def stim_circuit_html_viewer( + circuit: stim.Circuit, + *, + background: stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface | dict[int, stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface] | None = None, + tile_color_func: Callable[[stimflow.Tile], tuple[float, float, float, float] | tuple[float, float, float] | str] | None = None, + width: int = 500, + height: int = 500, + known_error: Iterable[stim.ExplainedError] | None = None, +) -> str_html: +``` + + +```python +# stimflow.stim_circuit_with_transformed_coords + +# (at top-level in the stimflow module) +def stim_circuit_with_transformed_coords( + circuit: stim.Circuit, + transform: Callable[[complex], complex], +) -> stim.Circuit: + """Returns an equivalent circuit, but with the qubit and detector position metadata modified. + The "position" is assumed to be the first two coordinates. These are mapped to the real and + imaginary values of a complex number which is then transformed. + + Note that `SHIFT_COORDS` instructions that modify the first two coordinates are not supported. + This is because supporting them requires flattening loops, or promising that the given + transformation is affine. + + Args: + circuit: The circuit with qubits to reposition. + transform: The transformation to apply to the positions. The positions are given one by one + to this method, as complex numbers. The method returns the new complex number for the + position. + + Returns: + The transformed circuit. + """ +``` + + +```python +# stimflow.stim_circuit_with_transformed_moments + +# (at top-level in the stimflow module) +def stim_circuit_with_transformed_moments( + circuit: stim.Circuit, + *, + moment_func: Callable[[stim.Circuit], stim.Circuit], +) -> stim.Circuit: + """Applies a transformation to regions of a circuit separated by TICKs and blocks. + + For example, in this circuit: + + H 0 + X 0 + TICK + + H 1 + X 1 + REPEAT 100 { + H 2 + X 2 + } + H 3 + X 3 + + TICK + H 4 + X 4 + + `moment_func` would be called five times, each time with one of the H and X instruction pairs. + The result from the method would then be substituted into the circuit, replacing each of the H + and X instruction pairs. + + Args: + circuit: The circuit to return a transformed result of. + moment_func: The transformation to apply to regions of the circuit. Returns a new circuit + for the result. + + Returns: + A transformed circuit. + """ +``` + + +```python +# stimflow.str_html + +# (at top-level in the stimflow module) +class str_html: + """A string that will display as an HTML widget in Jupyter notebooks. + + It's expected that the contents of the string will correspond to the + contents of an HTML file. + """ +``` + + +```python +# stimflow.str_html.write_to + +# (in class stimflow.str_html) +def write_to( + self, + path: str | pathlib.Path | io.IOBase, +): + """Write the contents to a file, and announce that it was done. + + This method exists for quick debugging. In many contexts, such as + in a bash terminal or in PyCharm, the printed path can be clicked + on to open the file. + """ +``` + + +```python +# stimflow.str_svg + +# (at top-level in the stimflow module) +class str_svg: + """A string that will display as an SVG image in Jupyter notebooks. + + It's expected that the contents of the string will correspond to the + contents of an SVG file. + """ +``` + + +```python +# stimflow.str_svg.write_to + +# (in class stimflow.str_svg) +def write_to( + self, + path: str | pathlib.Path | io.IOBase, +): + """Write the contents to a file, and announce that it was done. + + This method exists for quick debugging. In many contexts, such as + in a bash terminal or in PyCharm, the printed path can be clicked + on to open the file. + """ +``` + + +```python +# stimflow.svg + +# (at top-level in the stimflow module) +def svg( + objects: Iterable[stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface | stim.Circuit], + *, + background: stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface | stim.Circuit | None = None, + title: str | list[str] | None = None, + canvas_height: int | None = None, + show_order: bool = False, + show_obs: bool = True, + opacity: float = 1, + show_measure_qubits: bool = True, + show_data_qubits: bool = False, + system_qubits: Iterable[complex] = (), + show_all_qubits: bool = False, + extra_used_coords: Iterable[complex] = (), + show_coords: bool = True, + find_logical_err_max_weight: int | None = None, + rows: int | None = None, + cols: int | None = None, + tile_color_func: Callable[[stimflow.Tile], str | tuple[float, float, float] | tuple[float, float, float, float] | None] | None = None, + stabilizer_style: "Literal['polygon, 'circles'] | None" = 'polygon, + observable_style: "Literal['label, 'polygon, 'circles']" = 'label, + show_frames: bool = True, + pad: float | None = None, +) -> stimflow.str_svg: + """Returns an SVG image of the given objects. + """ +``` + + +```python +# stimflow.transpile_to_z_basis_interaction_circuit + +# (at top-level in the stimflow module) +def transpile_to_z_basis_interaction_circuit( + circuit: stim.Circuit, + *, + is_entire_circuit: bool = True, +) -> stim.Circuit: + """Converts to a circuit using CZ, iSWAP, and MZZ as appropriate. + + This method mostly focuses on inserting single qubit rotations to convert + interactions into their Z basis variant. It also does some optimizations + that remove redundant rotations which would tend to be introduced by this + process. + """ +``` + + +```python +# stimflow.transversal_code_transition_chunks + +# (at top-level in the stimflow module) +def transversal_code_transition_chunks( + *, + prev_code: StabilizerCode, + next_code: StabilizerCode, + measured: PauliMap, + reset: PauliMap, +) -> tuple[Chunk, ChunkReflow, Chunk]: +``` + + +```python +# stimflow.verify_distance_is_at_least_2 + +# (at top-level in the stimflow module) +def verify_distance_is_at_least_2( + obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode, +): +``` + + +```python +# stimflow.verify_distance_is_at_least_3 + +# (at top-level in the stimflow module) +def verify_distance_is_at_least_3( + obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode, +): +``` + + +```python +# stimflow.xor_sorted + +# (at top-level in the stimflow module) +def xor_sorted( + vals: Iterable[TItem], + *, + key: Callable[[TItem], Any] | None = None, +) -> list[TItem]: + """Sorts items and then cancels pairs of equal items. + + An item will be in the result once if it appeared an odd number of times. + An item won't be in the result if it appeared an even number of times. + + Args: + vals: The items to sort. + key: An optional key function, mapping the items to keys that determine the + sorted order. Unequal items with the same key don't cancel. + """ +``` diff --git a/glue/stimflow/doc/getting_started.ipynb b/glue/stimflow/doc/getting_started.ipynb new file mode 100644 index 00000000..a76d8f99 --- /dev/null +++ b/glue/stimflow/doc/getting_started.ipynb @@ -0,0 +1,7656 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b8e26f15-4d6e-4578-b9cf-dad22634e9bf", + "metadata": {}, + "source": [ + "# Example of using stimflow to create a surface memory experiment circuit\n", + "\n", + "# Step 1: import stimflow" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "112b8539-6b49-4f2f-bf93-c18befe590a4", + "metadata": {}, + "outputs": [], + "source": [ + "# HACK: manually get `stimflow` into the PATH.\n", + "# Assumes this notebook is being run from the doc directory that it is normally saved in.\n", + "import os\n", + "import sys\n", + "import pathlib\n", + "assert os.getcwd().endswith('stimflow/doc'), os.getcwd()\n", + "stimflow_path = str((pathlib.Path(os.getcwd()).parent / \"src\").absolute())\n", + "if stimflow_path not in sys.path:\n", + " sys.path.append(stimflow_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ac8dd654-0217-487f-ac0c-5345805feaeb", + "metadata": {}, + "outputs": [], + "source": [ + "import stimflow\n", + "import stim" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d55eeae4-2bfe-4c87-bd85-ad544cedb93b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "stimflow.__version__='0.1.0'\n", + "stim.__version__='1.16.dev1778114346'\n" + ] + } + ], + "source": [ + "print(f\"{stimflow.__version__=}\")\n", + "print(f\"{stim.__version__=}\")" + ] + }, + { + "cell_type": "markdown", + "id": "baa7dbcf-f747-475e-b796-317a19915dbc", + "metadata": {}, + "source": [ + "# Step 2: define the layout used by the surface code" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "27885aea-8612-47e9-a419-1788c12e55d2", + "metadata": {}, + "outputs": [], + "source": [ + "def make_surface_code(diameter: int) -> stimflow.StabilizerCode:\n", + " tiles = []\n", + "\n", + " for x in range(-1, diameter):\n", + " for y in range(-1, diameter):\n", + " m = x + 1j * y + 0.5 + 0.5j\n", + " potential_data = [m + 1j**k * (0.5 + 0.5j) for k in range(4)]\n", + " data = [d for d in potential_data if 0 <= d.real < diameter if 0 <= d.imag < diameter]\n", + " if len(data) not in [2, 4]:\n", + " continue\n", + "\n", + " basis = \"XZ\"[(x.real + y.real) % 2 == 0]\n", + " if not (0 <= m.real < diameter - 1) and basis != \"Z\":\n", + " continue\n", + " if not (0 <= m.imag < diameter - 1) and basis != \"X\":\n", + " continue\n", + " tiles.append(stimflow.Tile(measure_qubit=m, data_qubits=data, bases=basis))\n", + "\n", + " patch = stimflow.Patch(tiles)\n", + " obs_x = stimflow.PauliMap({q: \"X\" for q in patch.data_set if q.real == 0}, name=\"LX\")\n", + " obs_z = stimflow.PauliMap({q: \"Z\" for q in patch.data_set if q.imag == 0}, name=\"LZ\")\n", + " return stimflow.StabilizerCode(patch, logicals=[(obs_x, obs_z)])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4c71974b-c452-4f3e-8bfc-94fb4ad25d30", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "X\n", + "X\n", + "X\n", + "X\n", + "Z\n", + "Z\n", + "Z\n", + "Z\n", + "Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "'\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nX\\nX\\nX\\nX\\nX\\nZ\\nZ\\nZ\\nZ\\nZ\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Test and show\n", + "\n", + "make_surface_code(4).verify()\n", + "make_surface_code(5).verify()\n", + "\n", + "make_surface_code(5).to_svg(show_measure_qubits=True, show_coords=False)" + ] + }, + { + "cell_type": "markdown", + "id": "636b36b2-dba5-4c3b-985a-1e8545b73436", + "metadata": {}, + "source": [ + "# Step 3: define idle chunk and init chunk" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "701eeba7-9352-422d-88fb-73b32cb679d2", + "metadata": {}, + "outputs": [], + "source": [ + "def make_surface_code_idle_chunk(code: stimflow.StabilizerCode) -> stimflow.Chunk:\n", + " builder = stimflow.ChunkBuilder(allowed_qubits=code.used_set)\n", + "\n", + " # Find X and Z basis measurement qubits.\n", + " mxs = {tile.measure_qubit for tile in code.patch if tile.basis == \"X\"}\n", + " mzs = {tile.measure_qubit for tile in code.patch if tile.basis == \"Z\"}\n", + "\n", + " # Reset measure qubits into their respective bases.\n", + " builder.append(\"RX\", mxs)\n", + " builder.append(\"RZ\", mzs)\n", + " builder.append(\"TICK\")\n", + "\n", + " # Generate the two qubit gate layers.\n", + " x_offsets = [0.5 + 0.5j, -0.5 + 0.5j, 0.5 - 0.5j, -0.5 - 0.5j]\n", + " z_offsets = [0.5 + 0.5j, 0.5 - 0.5j, -0.5 + 0.5j, -0.5 - 0.5j]\n", + " for layer in range(4):\n", + " cxs: list[tuple[complex, complex]] = []\n", + " for tile in code.tiles:\n", + " offsets = x_offsets if tile.basis == \"X\" else z_offsets\n", + " offset: complex = offsets[layer]\n", + " m: complex = tile.measure_qubit\n", + " d: complex = m + offset\n", + " if d in code.data_set:\n", + " if tile.basis == \"X\":\n", + " cxs.append((m, d))\n", + " else:\n", + " cxs.append((d, m))\n", + " builder.append(\"CX\", cxs)\n", + " builder.append(\"TICK\")\n", + "\n", + " # Measure the measure qubits in their respective bases.\n", + " builder.append(\"MX\", mxs)\n", + " builder.append(\"MZ\", mzs)\n", + "\n", + " # Annotate the expected flows implemented by the circuit.\n", + " for tile in code.tiles:\n", + " builder.add_flow(start=tile, ms=[tile.measure_qubit])\n", + " builder.add_flow(end=tile, ms=[tile.measure_qubit])\n", + " for obs in code.flat_logicals:\n", + " builder.add_flow(start=obs, end=obs)\n", + "\n", + " return builder.finish_chunk()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "99597653-2f9b-4b10-a67b-a22be00bf249", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "
\n", + "
Loading...
\n", + " \n", + " \n", + " Open in Crumble\n", + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + "" + ], + "text/plain": [ + "'\\n\\n\\n\\n \\n\\n\\n
\\n
Loading...
\\n \\n \\n Open in Crumble\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
\\n \\n
\\n'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Test and show\n", + "\n", + "make_surface_code_idle_chunk(make_surface_code(4)).verify()\n", + "make_surface_code_idle_chunk(make_surface_code(5)).verify()\n", + "\n", + "make_surface_code_idle_chunk(make_surface_code(5)).to_html_viewer()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6b107526-e5ae-46c3-990f-84557dca87c8", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "RZ\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "\n", + "MZ\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "0,5\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "1\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "2\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "3\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "4\n", + "\n", + "" + ], + "text/plain": [ + "'\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRX\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\nRZ\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMX\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n\\nMZ\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n0,5\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n1\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n2\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n3\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n4\\n\\n'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stimflow.svg(\n", + " [make_surface_code_idle_chunk(make_surface_code(5)).to_coord_circuit()],\n", + " background=make_surface_code(5).patch,\n", + " show_coords=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "02be285a-cad5-4841-910f-dd228c7210fe", + "metadata": {}, + "outputs": [], + "source": [ + "def make_surface_code_init_chunk(code: stimflow.StabilizerCode, init_basis: str) -> stimflow.Chunk:\n", + " assert init_basis == \"X\" or init_basis == \"Z\", f\"{init_basis=}\"\n", + " builder = stimflow.ChunkBuilder(allowed_qubits=code.used_set)\n", + "\n", + " # Init all data qubits in the specified basis.\n", + " builder.append(f\"R{init_basis}\", code.data_set)\n", + "\n", + " # Annotate the expected flows.\n", + " # - Stabilizers and observables matching the init basis are initialized by the resets.\n", + " # - Stabilizers and observables not matching the init basis are explicitly discarded.\n", + " # (otherwise an error would occur when they are missing later during compilation.)\n", + " for tile in code.tiles:\n", + " if tile.basis == init_basis:\n", + " builder.add_flow(end=tile)\n", + " else:\n", + " builder.add_discarded_flow_output(tile)\n", + " for obs in code.flat_logicals:\n", + " if all(b == init_basis for b in obs.values()):\n", + " builder.add_flow(end=obs)\n", + " else:\n", + " builder.add_discarded_flow_output(obs)\n", + "\n", + " return builder.finish_chunk(wants_to_merge_with_next=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "fd2f5392-239c-47c4-aa66-8177667ae517", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "
\n", + "
Loading...
\n", + " \n", + " \n", + " Open in Crumble\n", + "
\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + "" + ], + "text/plain": [ + "'\\n\\n\\n\\n \\n\\n\\n
\\n
Loading...
\\n \\n \\n Open in Crumble\\n
\\n\\n\\n\\n
\\n \\n
\\n'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Test and show\n", + "\n", + "make_surface_code_init_chunk(make_surface_code(4), init_basis=\"X\").verify()\n", + "make_surface_code_init_chunk(make_surface_code(4), init_basis=\"Z\").verify()\n", + "make_surface_code_init_chunk(make_surface_code(5), init_basis=\"Z\").verify()\n", + "\n", + "make_surface_code_init_chunk(make_surface_code(5), init_basis=\"Z\").to_html_viewer()" + ] + }, + { + "cell_type": "markdown", + "id": "4af67b8b-a342-453c-b74a-79c0133b4041", + "metadata": {}, + "source": [ + "# Step 4: assemble chunks into a complete circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c7f01d99-bcd0-424c-8e11-9eaf44281c61", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "def make_surface_code_memory_circuit(code: stimflow.StabilizerCode, rounds: int) -> stim.Circuit:\n", + " compiler = stimflow.ChunkCompiler()\n", + "\n", + " transversal_init = make_surface_code_init_chunk(code, \"X\")\n", + " idle = make_surface_code_idle_chunk(code)\n", + " transversal_measure = transversal_init.time_reversed()\n", + " \n", + " compiler.append(transversal_init)\n", + " compiler.append(idle)\n", + " compiler.append(idle)\n", + " compiler.append(idle)\n", + " compiler.append(transversal_measure)\n", + "\n", + " return compiler.finish_circuit()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d8c883ec-9ffc-4115-bc99-bf6dfd1ad508", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ERROR! Session/line number was not unique in database. History logging moved to new session 3\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "Tick 0\n", + "\n", + "Tick 1\n", + "\n", + "Tick 2\n", + "\n", + "Tick 3\n", + "\n", + "Tick 4\n", + "\n", + "Tick 5\n", + "\n", + "Tick 6\n", + "\n", + "Tick 7\n", + "\n", + "Tick 8\n", + "\n", + "Tick 9\n", + "\n", + "Tick 10\n", + "\n", + "Tick 11\n", + "\n", + "Tick 12\n", + "\n", + "Tick 13\n", + "\n", + "Tick 14\n", + "\n", + "Tick 15\n", + "\n", + "Tick 16\n", + "\n", + "Tick 17\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "RX\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "R\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "M\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "MX\n", + "\n", + "Tick 0\n", + "\n", + "Tick 1\n", + "\n", + "Tick 2\n", + "\n", + "Tick 3\n", + "\n", + "Tick 4\n", + "\n", + "Tick 5\n", + "\n", + "Tick 6\n", + "\n", + "Tick 7\n", + "\n", + "Tick 8\n", + "\n", + "Tick 9\n", + "\n", + "Tick 10\n", + "\n", + "Tick 11\n", + "\n", + "Tick 12\n", + "\n", + "Tick 13\n", + "\n", + "Tick 14\n", + "\n", + "Tick 15\n", + "\n", + "Tick 16\n", + "\n", + "Tick 17\n", + "\n", + "\n", + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "code = make_surface_code(4)\n", + "circuit = make_surface_code_memory_circuit(code, rounds=3)\n", + "circuit.diagram(\"detslice-with-ops-svg\", rows=3)" + ] + }, + { + "cell_type": "markdown", + "id": "925d2d1f-b3c1-4ab1-bfed-14985f7602fe", + "metadata": {}, + "source": [ + "# Step 5: transpile to CZ gates and add noise" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "5e896624-af11-4833-956d-6eb2a47b3726", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "
\n", + "
Loading...
\n", + " \n", + " \n", + " Open in Crumble\n", + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + "" + ], + "text/plain": [ + "'\\n\\n\\n\\n \\n\\n\\n
\\n
Loading...
\\n \\n \\n Open in Crumble\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
\\n \\n
\\n'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "code = make_surface_code(4)\n", + "cx_circuit = make_surface_code_memory_circuit(code, rounds=3)\n", + "cz_circuit = stimflow.transpile_to_z_basis_interaction_circuit(cx_circuit)\n", + "\n", + "noise_model = stimflow.NoiseModel.si1000(1e-3)\n", + "noisy_circuit = noise_model.noisy_circuit(cz_circuit)\n", + "actual_distance = len(noisy_circuit.shortest_graphlike_error())\n", + "assert actual_distance == 4\n", + "\n", + "stimflow.stim_circuit_html_viewer(noisy_circuit, background=code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b10372c-4344-461d-af41-500894f9287a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/glue/stimflow/requirements.txt b/glue/stimflow/requirements.txt new file mode 100644 index 00000000..829bd1fa --- /dev/null +++ b/glue/stimflow/requirements.txt @@ -0,0 +1,2 @@ +pygltflib +stim diff --git a/glue/stimflow/setup.py b/glue/stimflow/setup.py new file mode 100644 index 00000000..f6368be7 --- /dev/null +++ b/glue/stimflow/setup.py @@ -0,0 +1,38 @@ +# Copyright 2021 Google LLC +# +# 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 +# +# http://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. + +from setuptools import setup + +with open('README.md', encoding='UTF-8') as f: + long_description = f.read() + +__version__ = '1.16.dev0' + +setup( + name='stimflow', + version=__version__, + author='Craig Gidney', + author_email='craig.gidney@gmail.com', + url='https://github.com/quantumlib/stim', + license='Apache 2', + packages=['stimflow'], + package_dir={'': 'src'}, + description='A library for creating quantum error correction circuits.', + long_description=long_description, + long_description_content_type='text/markdown', + python_requires='>=3.6.0', + data_files=['README.md'], + install_requires=['stim'], + tests_require=['pytest', 'python3-distutils'], +) diff --git a/glue/stimflow/src/stimflow/__init__.py b/glue/stimflow/src/stimflow/__init__.py new file mode 100644 index 00000000..a5acce57 --- /dev/null +++ b/glue/stimflow/src/stimflow/__init__.py @@ -0,0 +1,60 @@ +__version__ = "0.1.0" + +from stimflow._chunk import ( + Chunk, + ChunkBuilder, + ChunkCompiler, + ChunkInterface, + ChunkLoop, + ChunkReflow, + circuit_to_cycle_code_slices, + compile_chunks_into_circuit, + find_d1_error, + find_d2_error, + FlowMetadata, + Patch, + StabilizerCode, + StimCircuitLoom, + transversal_code_transition_chunks, + verify_distance_is_at_least_2, + verify_distance_is_at_least_3, +) +from stimflow._core import ( + append_reindexed_content_to_circuit, + circuit_to_dem_target_measurement_records_map, + circuit_with_xz_flipped, + complex_key, + count_measurement_layers, + Flow, + gate_counts_for_circuit, + gates_used_by_circuit, + min_max_complex, + NoiseModel, + NoiseRule, + PauliMap, + sorted_complex, + stim_circuit_with_transformed_coords, + stim_circuit_with_transformed_moments, + str_html, + str_svg, + Tile, + xor_sorted, +) +from stimflow._layers import ( + InteractLayer, + LayerCircuit, + MeasureLayer, + ResetLayer, + RotationLayer, + transpile_to_z_basis_interaction_circuit, +) +from stimflow._viz import ( + LineData, + TriangleData, + Viewable3dModelGLTF, + html_viewer_for_gltf_model, + make_3d_model, + stim_circuit_html_viewer, + svg, + TextData, +) diff --git a/glue/stimflow/src/stimflow/_chunk/__init__.py b/glue/stimflow/src/stimflow/_chunk/__init__.py new file mode 100644 index 00000000..c53b361f --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/__init__.py @@ -0,0 +1,20 @@ +"""Utilities for building/combining pieces of quantum error correction circuits.""" + +from stimflow._chunk._chunk import Chunk +from stimflow._chunk._chunk_builder import ChunkBuilder +from stimflow._chunk._chunk_compiler import ChunkCompiler, compile_chunks_into_circuit +from stimflow._chunk._chunk_interface import ChunkInterface +from stimflow._chunk._chunk_loop import ChunkLoop +from stimflow._chunk._chunk_reflow import ChunkReflow +from stimflow._chunk._code_util import ( + circuit_to_cycle_code_slices, + find_d1_error, + find_d2_error, + transversal_code_transition_chunks, + verify_distance_is_at_least_2, + verify_distance_is_at_least_3, +) +from stimflow._chunk._flow_metadata import FlowMetadata +from stimflow._chunk._patch import Patch +from stimflow._chunk._stabilizer_code import StabilizerCode +from stimflow._chunk._weave import StimCircuitLoom diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk.py b/glue/stimflow/src/stimflow/_chunk/_chunk.py new file mode 100644 index 00000000..f066716f --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk.py @@ -0,0 +1,862 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable, Iterable +from typing import Any, cast, Literal, TYPE_CHECKING + +import stim + +from stimflow._chunk._code_util import ( + verify_distance_is_at_least_2, + verify_distance_is_at_least_3, +) +from stimflow._chunk._patch import Patch +from stimflow._chunk._stabilizer_code import StabilizerCode +from stimflow._chunk._test_util import assert_has_same_set_of_items_as +from stimflow._core import ( + circuit_to_dem_target_measurement_records_map, + circuit_with_xz_flipped, + Flow, + NoiseModel, + PauliMap, + stim_circuit_with_transformed_coords, + str_html, + Tile, +) + +if TYPE_CHECKING: + from stimflow._chunk._chunk_interface import ChunkInterface + from stimflow._chunk._chunk_loop import ChunkLoop + from stimflow._chunk._chunk_reflow import ChunkReflow + + +class Chunk: + """A circuit chunk with accompanying stabilizer flow assertions.""" + + def __init__( + self, + circuit: stim.Circuit, + *, + flows: Iterable[Flow], + discarded_inputs: Iterable[PauliMap | Tile] = (), + discarded_outputs: Iterable[PauliMap | Tile] = (), + wants_to_merge_with_next: bool = False, + wants_to_merge_with_prev: bool = False, + q2i: dict[complex, int] | None = None, + o2i: dict[Any, int] | None = None, + ): + """ + + Args: + circuit: The circuit implementing the chunk's functionality. + flows: A series of stabilizer flows that the circuit implements. + discarded_inputs: Explicitly rejected in flows. For example, a data + measurement chunk might reject flows for stabilizers from the + anticommuting basis. If they are not rejected, then compilation + will fail when attempting to combine this chunk with a preceding + chunk that has those stabilizers from the anticommuting basis + flowing out. + discarded_outputs: Explicitly rejected out flows. For example, an + initialization chunk might reject flows for stabilizers from the + anticommuting basis. If they are not rejected, then compilation + will fail when attempting to combine this chunk with a following + chunk that has those stabilizers from the anticommuting basis + flowing in. + wants_to_merge_with_next: Defaults to False. When set to True, + the chunk compiler won't insert a TICK between this chunk + and the next chunk. + wants_to_merge_with_prev: Defaults to False. When set to True, + the chunk compiler won't insert a TICK between this chunk + and the previous chunk. + q2i: Defaults to None (infer from QUBIT_COORDS instructions in circuit else + raise an exception). The stimflow-qubit-coordinate-to-stim-qubit-index mapping + used to translate between stimflow's qubit keys and stim's qubit keys. + o2i: Defaults to None (raise an exception if observables present in circuit). + The stimflow-observable-key-to-stim-observable-index mapping used to translate + between stimflow's observable keys and stim's observable keys. + """ + if q2i is None: + q2i = {x + 1j * y: i for i, (x, y) in circuit.get_final_qubit_coordinates().items()} + if len(q2i) != circuit.num_qubits: + raise ValueError( + "The given circuit doesn't have enough `QUBIT_COORDS` instructions to " + "determine the stimflow-coordinate-to-stim-qubit-index mapping. You must manually " + "specify it by passing a `q2i={...}` argument, or add the missing " + "`QUBIT_COORDS`." + ) + flows = tuple(flows) + if o2i is None: + if circuit.num_observables: + raise ValueError( + "The given circuit has `OBSERVABLE_INCLUDE` instructions. You must specify " + "the stimflow-observable-key-to-stim-observable-index mapping by passing an" + "`o2k={...}` argument." + ) + o2i = {} + for flow in flows: + if flow.obs_key is not None and flow.obs_key not in o2i: + o2i[flow.obs_key] = len(o2i) + + self.q2i: dict[complex, int] = q2i + self.o2i: dict[Any, int] = o2i + self.circuit: stim.Circuit = circuit + self.flows: tuple[Flow, ...] = flows + + self.discarded_inputs: tuple[PauliMap, ...] = tuple( + e.to_pauli_map() if isinstance(e, Tile) else e for e in discarded_inputs + ) + self.discarded_outputs: tuple[PauliMap, ...] = tuple( + e.to_pauli_map() if isinstance(e, Tile) else e for e in discarded_outputs + ) + self.wants_to_merge_with_next = wants_to_merge_with_next + self.wants_to_merge_with_prev = wants_to_merge_with_prev + assert all(isinstance(e, PauliMap) for e in self.discarded_inputs) + assert all(isinstance(e, PauliMap) for e in self.discarded_outputs) + + def __add__(self, other: Chunk | ChunkReflow | ChunkLoop) -> Chunk: + return self.then(other) + + def then(self, other: Chunk | ChunkReflow | ChunkLoop) -> Chunk: + from stimflow._chunk._chunk_loop import ChunkLoop + from stimflow._chunk._chunk_reflow import ChunkReflow + + if isinstance(other, Chunk): + return self._then_chunk(other) + elif isinstance(other, ChunkReflow): + return self._then_reflow(other) + elif isinstance(other, ChunkLoop): + acc = self + for k in range(other.repetitions): + for c in other.chunks: + acc = self.then(c) + return acc + else: + raise NotImplementedError(f"{other=}") + + def _then_reflow(self, other: ChunkReflow) -> Chunk: + new_flows: list[Flow] = [] + new_discarded_outputs: list[PauliMap] = [] + + must_match_outputs: set[PauliMap] = set() + used_outputs: set[PauliMap] = set() + + i2f = {} + old_discarded_outputs = set(self.discarded_outputs) + must_match_outputs.update(self.discarded_outputs) + for flow in self.flows: + if flow.end: + assert flow.end not in i2f + i2f[flow.end] = flow + must_match_outputs.add(flow.end) + else: + new_flows.append(flow) + for out, inputs in other.out2in.items(): + acc = None + used_outputs.update(inputs) + for inp in inputs: + if inp in old_discarded_outputs: + new_discarded_outputs.append(out) + break + f = i2f[inp].with_edits(obs_key=None) + if acc is None: + acc = f + else: + acc *= f + else: + assert acc is not None + assert acc.end == out + new_flows.append(acc.with_edits(obs_key=out.name)) + if used_outputs != must_match_outputs: + lines = ["Unmatched reflows."] + for e in must_match_outputs - used_outputs: + lines.append(f" missing: {e}") + for e in used_outputs - must_match_outputs: + lines.append(f" extra: {e}") + raise ValueError("\n".join(lines)) + + result = Chunk( + circuit=self.circuit.copy(), + flows=new_flows, + discarded_inputs=self.discarded_inputs, + discarded_outputs=new_discarded_outputs, + wants_to_merge_with_prev=self.wants_to_merge_with_prev, + wants_to_merge_with_next=self.wants_to_merge_with_next, + ) + return result + + def _then_chunk(self, other: Chunk) -> Chunk: + from stimflow._chunk._chunk_compiler import ChunkCompiler + + compiler = ChunkCompiler() + compiler.append(self.with_edits(flows=[], discarded_inputs=[], discarded_outputs=[])) + compiler.append(other.with_edits(flows=[], discarded_inputs=[], discarded_outputs=[])) + combined_circuit = compiler.finish_circuit() + + nm1 = self.circuit.num_measurements + nm2 = other.circuit.num_measurements + + new_flows: list[Flow] = [] + new_discarded_outputs: list[PauliMap] = list(other.discarded_outputs) + new_discarded_inputs: list[PauliMap] = list(self.discarded_inputs) + + mid2flow: dict[PauliMap, Flow | Literal["discard"]] = {} + for flow in self.flows: + if flow.end: + mid2flow[flow.end] = flow + else: + new_flows.append(flow) + for key in self.discarded_outputs: + mid2flow[key] = "discard" + + for key in other.discarded_inputs: + prev_flow = mid2flow.pop(key, None) + if prev_flow is None: + lines = [] + lines.append("Incompatible chunks.") + lines.append(f"The second chunk has the discarded input {key}") + lines.append("But the first chunk has no matching flow output:") + e = self.end_interface() + for v in sorted(e.ports): + lines.append(f" {v}") + for v in sorted(e.discards): + lines.append(f" {v} [discard]") + raise ValueError("\n".join(lines)) + if isinstance(prev_flow, Flow): + if prev_flow.start: + new_discarded_inputs.append(prev_flow.start) + + for flow in other.flows: + if not flow.start: + new_flows.append( + flow.with_edits( + measurement_indices=[m % nm2 + nm1 for m in flow.measurement_indices] + ) + ) + continue + + prev_flow = mid2flow.pop(flow.start, None) + if prev_flow is None: + lines = [] + lines.append("Incompatible chunks.") + lines.append(f"The second chunk has the flow {flow}") + lines.append("But the first chunk has no matching flow output:") + e = self.end_interface() + for v in sorted(e.ports): + lines.append(f" {v}") + for v in sorted(e.discards): + lines.append(f" {v} [discard]") + raise ValueError("\n".join(lines)) + + if isinstance(prev_flow, Flow): + new_flows.append( + flow.with_edits( + start=prev_flow.start, + measurement_indices=[m % nm1 for m in prev_flow.measurement_indices] + + [m % nm2 + nm1 for m in flow.measurement_indices], + flags=flow.flags | prev_flow.flags, + ) + ) + else: + assert prev_flow == "discard" + if flow.end: + new_discarded_outputs.append(flow.end) + + for flow in new_flows: + if flow.obs_key is not None: + compiler.o2i.setdefault(flow.obs_key, len(compiler.o2i)) + result = Chunk( + circuit=combined_circuit, + q2i=compiler.q2i, + o2i=compiler.o2i, + flows=new_flows, + discarded_inputs=new_discarded_inputs, + discarded_outputs=new_discarded_outputs, + wants_to_merge_with_prev=self.wants_to_merge_with_prev, + wants_to_merge_with_next=other.wants_to_merge_with_next, + ) + return result + + def __repr__(self) -> str: + lines = ["stimflow.Chunk("] + lines.append(f" q2i={self.q2i!r},") + lines.append(f" circuit={self.circuit!r},".replace("\n", "\n ")) + if self.flows: + lines.append(f" flows={self.flows!r},") + if self.discarded_inputs: + lines.append(f" discarded_inputs={self.discarded_inputs!r},") + if self.discarded_outputs: + lines.append(f" discarded_outputs={self.discarded_outputs!r},") + if self.wants_to_merge_with_prev: + lines.append(f" wants_to_merge_with_prev={self.wants_to_merge_with_prev!r},") + if self.wants_to_merge_with_next: + lines.append(f" discarded_outputs={self.wants_to_merge_with_next!r},") + lines.append(")") + return "\n".join(lines) + + def with_obs_flows_as_det_flows(self) -> Chunk: + return self.with_edits(flows=[flow.with_edits(obs_key=None) for flow in self.flows]) + + def with_flag_added_to_all_flows(self, flag: str) -> Chunk: + return self.with_edits( + flows=[flow.with_edits(flags={*flow.flags, flag}) for flow in self.flows] + ) + + @staticmethod + def from_circuit_with_mpp_boundaries(circuit: stim.Circuit) -> Chunk: + allowed = {"TICK", "OBSERVABLE_INCLUDE", "DETECTOR", "MPP", "QUBIT_COORDS", "SHIFT_COORDS"} + start = 0 + end = len(circuit) + while start < len(circuit) and circuit[start].name in allowed: + start += 1 + while end > 0 and circuit[end - 1].name in allowed: + end -= 1 + while end < len(circuit) and circuit[end].name != "MPP": + end += 1 + while end > 0 and circuit[end - 1].name == "TICK": + end -= 1 + if end <= start: + raise ValueError("end <= start") + + prefix, body, suffix = circuit[:start], circuit[start:end], circuit[end:] + start_tick = prefix.num_ticks + end_tick = start_tick + body.num_ticks + 1 + c = stim.Circuit() + c += prefix + c.append("TICK") + c += body + c.append("TICK") + c += suffix + det_regions = c.detecting_regions(ticks=[start_tick, end_tick]) + records = circuit_to_dem_target_measurement_records_map(c) + pn = prefix.num_measurements + record_range = range(pn, pn + body.num_measurements) + + q2i = {qr + qi * 1j: i for i, (qr, qi) in circuit.get_final_qubit_coordinates().items()} + i2q = {i: q for q, i in q2i.items()} + dropped_detectors = set() + + flows = [] + for target, items in det_regions.items(): + if target.is_relative_detector_id(): + dropped_detectors.add(target.val) + start_ps: stim.PauliString = items.get(start_tick, stim.PauliString(0)) + end_ps: stim.PauliString = items.get(end_tick, stim.PauliString(0)) + + start_pm = PauliMap(start_ps).with_transformed_coords(lambda i: i2q[i]) + end_pm = PauliMap(end_ps).with_transformed_coords(lambda i: i2q[i]) + + flows.append( + Flow( + start=start_pm, + end=end_pm, + mids=[m - record_range.start for m in records[target] if m in record_range], + obs_key=None if target.is_relative_detector_id() else target.val, + center=(sum(start_pm.keys()) + sum(end_pm.keys())) + / (len(start_pm) + len(end_pm)), + ) + ) + + kept = stim.Circuit() + num_d = prefix.num_detectors + for inst in body.flattened(): + if inst.name == "DETECTOR": + if num_d not in dropped_detectors: + kept.append(inst) + num_d += 1 + elif inst.name != "OBSERVABLE_INCLUDE": + kept.append(inst) + o2i = {i: i for i in range(circuit.num_observables)} + + return Chunk(q2i=q2i, o2i=o2i, flows=flows, circuit=kept) + + def _interface( + self, + side: Literal["start", "end"], + *, + skip_discards: bool = False, + skip_passthroughs: bool = False, + ) -> tuple[PauliMap, ...]: + if side == "start": + include_start = True + include_end = False + elif side == "end": + include_start = False + include_end = True + else: + raise NotImplementedError(f"{side=}") + + result: list[PauliMap] = [] + for flow in self.flows: + if include_start and flow.start and not (skip_passthroughs and flow.end): + result.append(flow.start) + if include_end and flow.end and not (skip_passthroughs and flow.start): + result.append(flow.end) + if include_start and not skip_discards: + result.extend(self.discarded_inputs) + if include_end and not skip_discards: + result.extend(self.discarded_outputs) + + result_set: set[PauliMap] = set() + collisions: set[PauliMap] = set() + for item in result: + if item in result_set: + collisions.add(item) + result_set.add(item) + + if collisions: + msg = [f"{side} interface had collisions:"] + for a, b in sorted(collisions): + msg.append(f" {a}, obs_key={b}") + raise ValueError("\n".join(msg)) + + return tuple(sorted(result_set)) + + def with_edits( + self, + *, + circuit: stim.Circuit | None = None, + q2i: dict[complex, int] | None = None, + flows: Iterable[Flow] | None = None, + discarded_inputs: Iterable[PauliMap] | None = None, + discarded_outputs: Iterable[PauliMap] | None = None, + wants_to_merge_with_prev: bool | None = None, + wants_to_merge_with_next: bool | None = None, + ) -> Chunk: + return Chunk( + circuit=self.circuit if circuit is None else circuit, + q2i=self.q2i if q2i is None else q2i, + flows=self.flows if flows is None else flows, + discarded_inputs=( + self.discarded_inputs if discarded_inputs is None else discarded_inputs + ), + discarded_outputs=( + self.discarded_outputs if discarded_outputs is None else discarded_outputs + ), + wants_to_merge_with_prev=( + self.wants_to_merge_with_prev + if wants_to_merge_with_prev is None + else wants_to_merge_with_prev + ), + wants_to_merge_with_next=( + self.wants_to_merge_with_next + if wants_to_merge_with_next is None + else wants_to_merge_with_next + ), + ) + + def __eq__(self, other): + if not isinstance(other, Chunk): + return NotImplemented + return ( + self.q2i == other.q2i + and self.circuit == other.circuit + and self.flows == other.flows + and self.discarded_inputs == other.discarded_inputs + and self.discarded_outputs == other.discarded_outputs + and self.wants_to_merge_with_prev == other.wants_to_merge_with_prev + and self.wants_to_merge_with_next == other.wants_to_merge_with_next + ) + + def to_html_viewer( + self, + *, + background: ( + Patch + | StabilizerCode + | ChunkInterface + | dict[int, Patch | StabilizerCode | ChunkInterface] + | None + ) = None, + tile_color_func: Callable[[Tile], tuple[float, float, float, float]] | None = None, + known_error: Iterable[stim.ExplainedError] | None = None, + ) -> str_html: + from stimflow._viz import stim_circuit_html_viewer + + circuit = self.to_closed_circuit() + if background is None: + start = self.start_patch() + end = self.end_patch() + if len(start.tiles) == 0: + background = end + elif len(end.tiles) == 0: + background = start + else: + background = {0: start, circuit.num_ticks: end} + return stim_circuit_html_viewer( + circuit, background=background, tile_color_func=tile_color_func, known_error=known_error + ) + + def __mul__(self, other: int) -> ChunkLoop: + from stimflow._chunk._chunk_loop import ChunkLoop + + return ChunkLoop([self], repetitions=other) + + def with_repetitions(self, repetitions: int) -> ChunkLoop: + from stimflow._chunk._chunk_loop import ChunkLoop + + return ChunkLoop([self], repetitions=repetitions) + + def find_distance( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 1e-3, + noiseless_qubits: Iterable[float | int | complex] = (), + skip_adding_noise: bool = False, + ) -> int: + err = self.find_logical_error( + max_search_weight=max_search_weight, + noise=noise, + noiseless_qubits=noiseless_qubits, + skip_adding_noise=skip_adding_noise, + ) + return len(err) + + def to_closed_circuit(self) -> stim.Circuit: + """Compiles the chunk into a circuit by conjugating with mpp init/end chunks.""" + from stimflow._chunk._chunk_compiler import ChunkCompiler + + compiler = ChunkCompiler() + compiler.append_magic_init_chunk(self.start_interface()) + compiler.append(self) + compiler.append_magic_end_chunk(self.end_interface()) + return compiler.finish_circuit() + + def verify_distance_is_at_least_2(self, *, noise: float | NoiseModel = 1e-3): + """Verifies undetected logical errors require at least 2 physical errors. + + By default, verifies using a uniform depolarizing circuit noise model. + """ + __tracebackhide__ = True + circuit = self.to_closed_circuit() + if isinstance(noise, float): + noise = NoiseModel.uniform_depolarizing(1e-3) + circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) + verify_distance_is_at_least_2(circuit) + + def to_coord_circuit(self) -> stim.Circuit: + coords = stim.Circuit() + for q, i in self.q2i.items(): + coords.append("QUBIT_COORDS", [i], [q.real, q.imag]) + return coords + self.circuit + + def verify_distance_is_at_least_3(self, *, noise: float | NoiseModel = 1e-3): + """Verifies undetected logical errors require at least 3 physical errors. + + By default, verifies using a uniform depolarizing circuit noise model. + """ + __tracebackhide__ = True + circuit = self.to_closed_circuit() + if isinstance(noise, float): + noise = NoiseModel.uniform_depolarizing(1e-3) + circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) + verify_distance_is_at_least_3(circuit) + + def find_logical_error( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 1e-3, + noiseless_qubits: Iterable[float | int | complex] = (), + skip_adding_noise: bool = False, + ) -> list[stim.ExplainedError]: + circuit = self.to_closed_circuit() + if not skip_adding_noise: + if isinstance(noise, float): + noise = NoiseModel.uniform_depolarizing(1e-3) + circuit = noise.noisy_circuit_skipping_mpp_boundaries( + circuit, immune_qubit_coords=noiseless_qubits + ) + if max_search_weight == 2: + return circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) + return circuit.search_for_undetectable_logical_errors( + dont_explore_edges_with_degree_above=max_search_weight, + dont_explore_detection_event_sets_with_size_above=max_search_weight, + dont_explore_edges_increasing_symptom_degree=False, + canonicalize_circuit_errors=True, + ) + + def verify( + self, + *, + expected_in: ChunkInterface | StabilizerCode | Patch | None = None, + expected_out: ChunkInterface | StabilizerCode | Patch | None = None, + should_measure_all_code_stabilizers: bool = False, + allow_overlapping_flows: bool = False, + ): + """Checks that this chunk's circuit actually implements its flows.""" + __tracebackhide__ = True + + # Check basic types. + assert ( + not should_measure_all_code_stabilizers + or expected_in is not None + or should_measure_all_code_stabilizers is not None + ) + assert isinstance(self.circuit, stim.Circuit) + assert isinstance(self.q2i, dict) + assert isinstance(self.o2i, dict) + assert isinstance(self.flows, tuple) + assert isinstance(self.discarded_inputs, tuple) + assert isinstance(self.discarded_outputs, tuple) + assert all(isinstance(e, Flow) for e in self.flows) + assert all(isinstance(e, PauliMap) for e in self.discarded_inputs) + assert all(isinstance(e, PauliMap) for e in self.discarded_outputs) + + # Check observable mapping. + i2o = {i: o for o, i in self.o2i.items()} + if len(i2o) < len(self.o2i): + raise ValueError(f"{self.o2i=} maps multiple observables to the same index.") + obs_indices_present: set[int] = set() + _accumulate_observable_indices_used_by_circuit(self.circuit, out=obs_indices_present) + unexplained_indices = obs_indices_present - i2o.keys() + if unexplained_indices: + raise ValueError( + f"The chunk's circuit has {obs_indices_present=}, but {self.o2i=} doesn't map to " + f"any of {unexplained_indices=}." + ) + + # Check for flow collisions. + if not allow_overlapping_flows: + groups: collections.defaultdict[PauliMap, list[Flow]] + + groups = collections.defaultdict(list) + for flow in self.flows: + groups[flow.start].append(flow) + for key, group in groups.items(): + if key and len(group) > 1: + lines = ["Multiple flows with same non-empty start:"] + for g in group: + lines.append(f" {g}") + raise ValueError("\n".join(lines)) + + groups = collections.defaultdict(list) + for flow in self.flows: + groups[flow.end].append(flow) + for key, group in groups.items(): + if key and len(group) > 1: + raise ValueError(f"Multiple flows with same non-empty end: {group}") + + # Verify flows are actually satisfied by the circuit. + unsigned_stim_flows: list[stim.Flow] = [] + unsigned_indices: list[int] = [] + signed_stim_flows: list[stim.Flow] = [] + signed_indices: list[int] = [] + o2i_def: collections.defaultdict[Any, int | None] = collections.defaultdict(lambda: None) + o2i_def.update(self.o2i) + for k, flow in enumerate(self.flows): + stim_flow = flow.to_stim_flow(q2i=self.q2i, o2i=o2i_def) + if flow.sign is None: + unsigned_stim_flows.append(stim_flow) + unsigned_indices.append(k) + else: + signed_stim_flows.append(stim_flow) + signed_indices.append(k) + if not self.circuit.has_all_flows( + unsigned_stim_flows, unsigned=True + ) or not self.circuit.has_all_flows(signed_stim_flows): + msg = [] + for k in range(len(unsigned_stim_flows)): + if not self.circuit.has_flow(unsigned_stim_flows[k], unsigned=True): + msg.append(" (unsigned) " + str(self.flows[unsigned_indices[k]])) + for k in range(len(signed_stim_flows)): + if not self.circuit.has_flow(signed_stim_flows[k], unsigned=True): + msg.append( + " (wanted signed, not even unsigned present) " + + str(self.flows[signed_indices[k]]) + ) + elif not self.circuit.has_flow(signed_stim_flows[k]): + msg.append(" (signed) " + str(self.flows[signed_indices[k]])) + msg.insert(0, f"Circuit lacks the following {len(msg)} flows:") + raise ValueError("\n".join(msg)) + + if expected_in is not None: + if isinstance(expected_in, Patch): + expected_in = StabilizerCode(expected_in).as_interface() + if isinstance(expected_in, StabilizerCode): + expected_in = expected_in.as_interface() + if should_measure_all_code_stabilizers: + assert_has_same_set_of_items_as( + self.start_interface(skip_passthroughs=True) + .without_discards() + .without_keyed() + .ports, + expected_in.without_discards().without_keyed().ports, + "actual_measured_operators", + "expected_measured_operators", + ) + assert_has_same_set_of_items_as( + self.start_interface().with_discards_as_ports().ports, + expected_in.with_discards_as_ports().ports, + "actual_start_interface", + "expected_start_interface", + ) + else: + # Creating the interface checks for collisions + self.start_interface() + + if expected_out is not None: + if isinstance(expected_out, Patch): + expected_out = StabilizerCode(expected_out).as_interface() + if isinstance(expected_out, StabilizerCode): + expected_out = expected_out.as_interface() + if should_measure_all_code_stabilizers: + assert_has_same_set_of_items_as( + self.end_interface(skip_passthroughs=True) + .without_discards() + .without_keyed() + .ports, + expected_out.without_discards().without_keyed().ports, + "actual_prepared_operators", + "expected_prepared_operators", + ) + assert_has_same_set_of_items_as( + self.end_interface().with_discards_as_ports().ports, + expected_out.with_discards_as_ports().ports, + "actual_end_interface", + "expected_end_interface", + ) + else: + # Creating the interface checks for collisions + self.end_interface() + + def time_reversed(self) -> Chunk: + """Checks that this chunk's circuit actually implements its flows.""" + + stim_flows = [] + for flow in self.flows: + inp = stim.PauliString(len(self.q2i)) + out = stim.PauliString(len(self.q2i)) + for q, p in flow.start.qubits.items(): + inp[self.q2i[q]] = p + for q, p in flow.end.qubits.items(): + out[self.q2i[q]] = p + stim_flows.append( + stim.Flow(input=inp, output=out, measurements=cast(Any, flow.measurement_indices)) + ) + rev_circuit, rev_flows = self.circuit.time_reversed_for_flows(stim_flows) + nm = rev_circuit.num_measurements + return Chunk( + circuit=rev_circuit, + q2i=self.q2i, + flows=[ + Flow( + center=flow.center, + start=flow.end, + end=flow.start, + mids=[m + nm for m in rev_flow.measurements_copy()], + flags=flow.flags, + obs_key=flow.obs_key, + ) + for flow, rev_flow in zip(self.flows, rev_flows, strict=True) + ], + discarded_inputs=self.discarded_outputs, + discarded_outputs=self.discarded_inputs, + wants_to_merge_with_prev=self.wants_to_merge_with_next, + wants_to_merge_with_next=self.wants_to_merge_with_prev, + ) + + def with_xz_flipped(self) -> Chunk: + return self.with_edits( + circuit=circuit_with_xz_flipped(self.circuit), + flows=[flow.with_xz_flipped() for flow in self.flows], + discarded_inputs=[p.with_xz_flipped() for p in self.discarded_inputs], + discarded_outputs=[p.with_xz_flipped() for p in self.discarded_outputs], + ) + + def with_transformed_coords(self, transform: Callable[[complex], complex]) -> Chunk: + return self.with_edits( + q2i={transform(q): i for q, i in self.q2i.items()}, + circuit=stim_circuit_with_transformed_coords(self.circuit, transform), + flows=[flow.with_transformed_coords(transform) for flow in self.flows], + discarded_inputs=[p.with_transformed_coords(transform) for p in self.discarded_inputs], + discarded_outputs=[ + p.with_transformed_coords(transform) for p in self.discarded_outputs + ], + ) + + def flattened(self) -> list[Chunk]: + """This is here for duck-type compatibility with ChunkLoop.""" + return [self] + + def start_interface(self, *, skip_passthroughs: bool = False) -> ChunkInterface: + """Returns a description of the flows that should enter into the chunk.""" + from stimflow._chunk._chunk_interface import ChunkInterface + + return ChunkInterface( + ports=[ + flow.start + for flow in self.flows + if flow.start + if not (skip_passthroughs and flow.end) + ], + discards=self.discarded_inputs, + ) + + def end_interface(self, *, skip_passthroughs: bool = False) -> ChunkInterface: + """Returns a description of the flows that should exit from the chunk.""" + from stimflow._chunk._chunk_interface import ChunkInterface + + return ChunkInterface( + ports=[ + flow.end + for flow in self.flows + if flow.end + if not (skip_passthroughs and flow.start) + ], + discards=self.discarded_outputs, + ) + + def start_code(self) -> StabilizerCode: + return StabilizerCode( + self.start_patch(), + logicals=[flow.start for flow in self.flows if flow.obs_key is not None], + ) + + def end_code(self) -> StabilizerCode: + return StabilizerCode( + self.end_patch(), + logicals=[flow.end for flow in self.flows if flow.obs_key is not None], + ) + + def start_patch(self) -> Patch: + from stimflow._chunk._patch import Patch + + return Patch( + [ + Tile( + bases="".join(flow.start.values()), + data_qubits=flow.start.keys(), + measure_qubit=flow.center, + flags=flow.flags, + ) + for flow in self.flows + if flow.start + if flow.obs_key is None + ] + ) + + def end_patch(self) -> Patch: + from stimflow._chunk._patch import Patch + + return Patch( + [ + Tile( + bases="".join(flow.end.values()), + data_qubits=flow.end.keys(), + measure_qubit=flow.center, + flags=flow.flags, + ) + for flow in self.flows + if flow.end + if flow.obs_key is None + ] + ) + + +def _accumulate_observable_indices_used_by_circuit(circuit: stim.Circuit, *, out: set[int]): + for inst in circuit: + if inst.name == "OBSERVABLE_INCLUDE": + out.add(int(inst.gate_args_copy()[0])) + elif inst.name == "REPEAT": + _accumulate_observable_indices_used_by_circuit(inst.body_copy(), out=out) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py new file mode 100644 index 00000000..20497af8 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py @@ -0,0 +1,746 @@ +from __future__ import annotations + +import sys +from collections.abc import Callable, Iterable, Sequence +from typing import Any, cast, Literal + +import stim + +from stimflow._chunk import Chunk +from stimflow._core._complex_util import sorted_complex, xor_sorted +from stimflow._core._flow import Flow +from stimflow._core._pauli_map import PauliMap +from stimflow._core._tile import Tile + +_SWAP_CONJUGATED_MAP = {"XCZ": "CX", "YCZ": "CY", "YCX": "XCY", "SWAPCX": "CXSWAP"} + + +class ChunkBuilder: + """Helper class for building stim circuits. + + Handles qubit indexing (complex -> int). + Handles measurement tracking (naming results and referring to them by name). + """ + + def __init__( + self, + allowed_qubits: Iterable[complex] | None = None, + ): + """Creates a Builder for creating a circuit over the given qubits. + + Args: + allowed_qubits: Defaults to None (everything allowed). Specifies the qubit positions + that the circuit is permitted to contain. + """ + self.allowed_qubits: set[complex] | None = None if allowed_qubits is None else set(allowed_qubits) + self._num_measurements: int = 0 + self._recorded_measurements: dict[Any, list[int]] = {} + self.circuit: stim.Circuit = stim.Circuit() + self.q2i: dict[complex, int] = {} + self.o2i: dict[Any, int] = {} + self.flows: list[Flow] = [] + self.flows_with_auto_ms: list[Flow] = [] + self.flows_with_auto_start: list[Flow] = [] + self.flows_with_auto_end: list[Flow] = [] + self.discarded_output_flows: list[PauliMap] = [] + self.discarded_input_flows: list[PauliMap] = [] + + # Index allowed qubits. + if allowed_qubits is not None: + for i, q in enumerate(sorted_complex(allowed_qubits)): + self.q2i[q] = i + + def _ensure_obs_index_of(self, name: Any) -> int: + result = self.o2i.get(name) + if result is None: + result = max(self.o2i.values(), default=-1) + 1 # TODO: avoid quadratic overhead + self.o2i[name] = result + return result + + def _ensure_indices( + self, + qs: Iterable[complex], + *, + context_gate: Any, + context_targets: Any, + context_arg: Any, + unknown_qubit_append_mode: Literal["auto", "error", "skip", "include"], + ) -> bool: + if unknown_qubit_append_mode == "auto": + if self.allowed_qubits is None: + unknown_qubit_append_mode = "include" + else: + unknown_qubit_append_mode = "error" + + missing = [q for q in qs if q not in self.q2i] + if not missing: + return True + + bad_types = [q for q in missing if not isinstance(q, (int, float, complex))] + if bad_types: + raise ValueError( + f"Expected qubit positions (an int, float, or complex), " + f"but got {bad_types[0]!r}.\n" + f" gate={context_gate!r}\n" + f" targets={context_targets!r}\n" + f" arg={context_arg!r}" + ) + + if unknown_qubit_append_mode == "error": + raise KeyError( + f"{unknown_qubit_append_mode=} but " + f"the qubit positions {missing!r} aren't " + f"in builder.allowed_qubits={self.allowed_qubits}, but " + f"{unknown_qubit_append_mode=}" + ) + elif unknown_qubit_append_mode == "include": + for q in missing: + i = len(self.q2i) + self.q2i[q] = i + return True + elif unknown_qubit_append_mode == "skip": + pass + else: + raise NotImplementedError(f"{unknown_qubit_append_mode=}") + return False + + def _rec(self, key: Any, value: list[int]) -> None: + if key in self._recorded_measurements: + raise ValueError( + f"Attempted to record a measurement for {key=}, but the key is already used." + ) + self._recorded_measurements[key] = value + + def record_measurement_group(self, sub_keys: Iterable[Any], *, key: Any) -> None: + """Combines multiple measurement keys into one key. + + Args: + sub_keys: The measurement keys to combine. + key: Where to store the combined result. + """ + self._rec(key, self.lookup_mids(sub_keys)) + + def has_measurement(self, key: Any) -> bool: + return key in self._recorded_measurements + + def lookup_mids(self, keys: Iterable[Any], *, ignore_unmatched: bool = False) -> list[int]: + """Looks up measurement indices by key. + + Measurement keys are created automatically when appending measurement operations into the + circuit via the builder's append method. They are also created manually by methods like + `builder.record_measurement_group`. + + Args: + keys: The measurement keys to lookup. + ignore_unmatched: Defaults to False. If set to True, keys that don't correspond + to measurements are ignored instead of raising an error. + + Returns: + A list of offsets indicating when the measurements occurred. + """ + result: list[int] = [] + missing: list[Any] = [] + if isinstance(keys, PauliMap): + raise ValueError( + f"Expected a list of measurement record keys, but got {keys=}.\n" + f"Did you forget to wrap it into a list?" + ) + for key in keys: + recs = self._recorded_measurements.get(key) + if recs is None: + missing.append(key) + else: + result.extend(recs) + if missing and not ignore_unmatched: + raise ValueError( + "Some of the given measurement record keys don't exist.\n" + f"Unmatched keys: {missing!r}\n" + f"Given keys: {list(keys)!r}" + ) + return xor_sorted(result) + + def add_discarded_flow_input(self, flow: PauliMap | Tile) -> None: + if isinstance(flow, Tile): + flow = flow.to_pauli_map() + self.discarded_input_flows.append(flow) + + def add_discarded_flow_output(self, flow: PauliMap | Tile) -> None: + if isinstance(flow, Tile): + flow = flow.to_pauli_map() + self.discarded_output_flows.append(flow) + + def add_flow( + self, + *, + start: PauliMap | Tile | Literal["auto"] | None = None, + end: PauliMap | Tile | Literal["auto"] | None = None, + ms: Iterable[Any] | Literal["auto"] = (), + ignore_unmatched_ms: bool = False, + obs_key: Any = None, + center: complex | None = None, + flags: Iterable[str] = frozenset(), + sign: bool | None = None, + ) -> None: + """Declares that the circuit being built should have a given stabilizer flow. + + When chunks are concatenated, their flows are paired up in order to form detectors. + + Args: + start: Defaults to None (empty). The stabilizer that the flow starts as, at the + beginning of the circuit. If the flow begins within the circuit, this should + be set to None or an empty PauliMap. + end: Defaults to None (empty). The stabilizer that the flow ends as, at the + end of the circuit. If the flow ends within the circuit, this should + be set to None or an empty PauliMap. + ms: Defaults to empty. The keys identifying measurements mediate the flow. + For example, if a stabilizer is measured by a circuit then this would + typically be a singleton list containing the measurement that reveals + the stabilizer's value. + ignore_unmatched_ms: Defaults to False. When set to False, unrecognized measurement + ids cause the method to raise an exception instead of adding the flow. When set + to True, unrecognized measurements are silently discarded. + obs_key: Defaults to None (not a logical operator). If this is set to a value other + than None, it identifies the logical operator whose flow the flow is describing. + center: Defaults to None (unused). Optional metadata specifying coordinates for the + flow. Typically these coordinates will end up being exposed as the parens args + on the DETECTOR instruction created when producing a stim circuit. When not + specified, the coordinates will instead be inferred in some heuristic way. + flags: Defaults to empty. Hashable equatable values associated with the flow. When + flows are combined, the result will contain the union of their flags. When compiling + chunks into a circuit, the optional `metadata_func` argument can use these flags + to produce better metadata. + sign: Defaults to None (unsigned). When not set, the circuit having the flow with either + a positive or negative sign are both acceptable. When set to False or True, the sign + implemented by the circuit must match. + + Examples: + >>> import stimflow as sf + >>> builder = sf.ChunkBuilder() + >>> builder.append('R', [0]) + >>> builder.append('MX', [1j]) + >>> builder.append('TICK') + >>> builder.append('CX', [(1j, 0)]) + + >>> builder.add_flow(end=sf.PauliMap.from_xs([0, 1j]), ms=[1j]) + >>> builder.add_flow(end=sf.PauliMap.from_zs([0, 1j])) + >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), ms=[1j]) + + >>> builder.finish_chunk().verify() + """ + auto_count = (start == "auto") + (end == "auto") + (ms == "auto") + if auto_count > 1: + raise ValueError("Only one of `start`, `end`, and `ms` can be set to auto.\n" + f" {start=}" + f" {ms=}" + f" {end=}") + out = self.flows + if start == "auto": + out = self.flows_with_auto_start + start = None + elif end == "auto": + out = self.flows_with_auto_end + end = None + elif ms == "auto": + out = self.flows_with_auto_ms + ms = () + + out.append( + Flow( + start=cast(PauliMap, start), + end=cast(PauliMap, end), + mids=self.lookup_mids(ms, ignore_unmatched=ignore_unmatched_ms), + obs_key=obs_key, + center=center, + flags=flags, + sign=sign, + ) + ) + + def finish_chunk( + self, + *, + wants_to_merge_with_prev: bool = False, + wants_to_merge_with_next: bool = False, + failure_mode: Literal["error", "ignore", "print"] = "error", + ) -> Chunk: + """Finishes producing the circuit.""" + + from stimflow._chunk._flow_util import _solve_auto_flow_starts + from stimflow._chunk._flow_util import _solve_auto_flow_ends + from stimflow._chunk._flow_util import _solve_auto_flow_ms + + start_fails = [] + measure_fails = [] + end_fails = [] + + solved_starts = _solve_auto_flow_starts( + flows=self.flows_with_auto_start, + circuit=self.circuit, + q2i=self.q2i, + failure_out=start_fails, + ) + solved_ends = _solve_auto_flow_ends( + flows=self.flows_with_auto_end, + circuit=self.circuit, + q2i=self.q2i, + failure_out=end_fails, + ) + solved_ms = _solve_auto_flow_ms( + flows=self.flows_with_auto_ms, + circuit=self.circuit, + q2i=self.q2i, + o2i=self.o2i, + failure_out=measure_fails, + ) + + out_circuit = self.circuit.copy() + if start_fails or end_fails or measure_fails: + lines = [] + if start_fails: + lines.append( + "Failed to auto-solve starts for the following flows:") + for flow in start_fails: + lines.append(" " + str(flow)) + if measure_fails: + lines.append("Failed to auto-solve measurements for the following flows:") + for flow in measure_fails: + lines.append(" " + str(flow)) + if end_fails: + lines.append( + "Failed to auto-solve ends for the following flows:") + for flow in end_fails: + lines.append(" " + str(flow)) + + if failure_mode == "print": + out_circuit = out_circuit.copy() + out_circuit.insert(0, stim.CircuitInstruction("TICK")) + out_circuit.append(stim.CircuitInstruction("TICK")) + for flow in end_fails + measure_fails: + if flow.start: + out_circuit.insert( + 0, + stim.CircuitInstruction( + "CORRELATED_ERROR", + [ + stim.target_pauli(self.q2i[q], p) + for q, p in cast(PauliMap, flow.start).items() + ], + [0], + tag="BAD-FLOW", + ), + ) + for flow in start_fails + measure_fails: + if flow.end: + out_circuit.append( + stim.CircuitInstruction( + "CORRELATED_ERROR", + [ + stim.target_pauli(self.q2i[q], p) + for q, p in cast(PauliMap, flow.end).items() + ], + [0], + tag="BAD-FLOW", + ) + ) + print('\n'.join(lines), file=sys.stderr) + elif failure_mode == "error": + raise ValueError('\n'.join(lines)) + + return Chunk( + circuit=out_circuit, + q2i=self.q2i, + o2i=self.o2i, + flows=self.flows + solved_starts + solved_ms + solved_ends, + discarded_inputs=self.discarded_input_flows, + discarded_outputs=self.discarded_output_flows, + wants_to_merge_with_next=wants_to_merge_with_next, + wants_to_merge_with_prev=wants_to_merge_with_prev, + ) + + def append( + self, + gate: str, + targets: Iterable[complex | Sequence[complex] | PauliMap | Tile | Any] = (), + *, + arg: float | Iterable[float] | None = None, + measure_key_func: ( + Callable[[complex], Any] + | Callable[[tuple[complex, complex]], Any] + | Callable[[PauliMap | Tile], Any] + | None + ) = lambda e: e, + tag: str = "", + unknown_qubit_append_mode: Literal["auto", "error", "skip", "include"] = "auto", + ) -> None: + """Appends an instruction to the builder's circuit. + + This method differs from `stim.Circuit.append` in the following ways: + + 1) It targets qubits by position instead of by index. Also, it takes two + qubit targets as pairs instead of interleaved. For example, instead of + saying + + a = builder.q2i[5 + 1j] + b = builder.q2i[5] + c = builder.q2i[0] + d = builder.q2i[1j] + builder.circuit.append('CZ', [a, b, c, d]) + + you would say + + builder.append('CZ', [(5+1j, 5), (0, 1j)]) + + 2) It canonicalizes. In particular, it will: + - Sort targets. For example: + `H 3 1 2` -> `H 1 2 3` + `CX 2 3 1 0` -> `CX 1 0 2 3` + `CZ 2 3 6 0` -> `CZ 0 6 2 3` + - Replace rare gates with common gates. For example: + `XCZ 1 2` -> `CX 2 1` + - Not append target-less gates at all. For example: + `CX ` -> `` + + Canonicalization makes the form of the final circuit stable, + despite things like python's `set` data structure having + inconsistent iteration orders. This makes the output easier + to unit test, and more viable to store under source control. + + 3) It tracks measurements. When appending a measurement, its index is + stored in the measurement tracker keyed by the position of the qubit + being measured (or by a custom key, if `measure_key_func` is specified). + The indices of the measurements can be looked up later via + `builder.lookup_mids([key1, key2, ...])`. + + Args: + gate: The name of the gate to append, such as "H" or "M" or "CX". + targets: The qubit positions that the gate operates on. For single + qubit gates like H or M this should be an iterable of complex + numbers. For two qubit gates like CX or MXX it should be an + iterable of pairs of complex numbers. For MPP it should be an + iterable of stimflow.PauliMap instances. + arg: Optional. The parens argument or arguments used for the gate + instruction. For example, for a measurement gate, this is the + probability of the incorrect result being reported. + measure_key_func: Customizes the keys used to track the indices of + measurement results. By default, measurements are keyed by + position, but thus won't work if a circuit measures the same + qubit multiple times. This function can transform that position + into a different value (for example, you might set + `measure_key_func=lambda pos: (pos, 'first_cycle')` for + measurements during the first cycle of the circuit. + tag: Defaults to "" (no tag). A custom tag to attach to the + instruction(s) appended into the stim circuit. + unknown_qubit_append_mode: Defaults to 'auto'. The available options are: + - 'auto': Replace by 'include' if the builder's `allowed_qubits` field is + empty, else replace by 'error'. + - 'error': When a qubit position outside `allowed_qubits` is encountered, + raise an exception. + - 'include': When a qubit position outside `allowed_qubits` is encountered, + automatically include it into `builder.q2i` and `builder.allowed_qubits`. + - 'skip': When a qubit position outside `allowed_qubits` is encountered, + ignore it. Note that, for two-qubit and multi-qubit operations, this + will ignore the pair or group of targets containing the skipped position. + """ + __tracebackhide__ = True + data = stim.gate_data(gate) + + if data.name == "TICK": + if arg is not None: + raise ValueError(f"TICK takes no arguments but got {arg=}.") + if targets: + raise ValueError(f"TICK takes no targets but got {targets=}.") + self.circuit.append("TICK", tag=tag) + + elif data.name == "SHIFT_COORDS": + if arg is None: + raise ValueError(f"SHIFT_COORDS expects {arg=} to not be None.") + if targets: + raise ValueError(f"SHIFT_COORDS takes no targets but got {targets=}.") + self.circuit.append("SHIFT_COORDS", [], arg, tag=tag) + + elif data.name == "DETECTOR" or data.name == "OBSERVABLE_INCLUDE": + if isinstance(targets, PauliMap) and data.name == "OBSERVABLE_INCLUDE": + if arg is None and targets.name is None: + raise ValueError( + "Received a stimflow.PauliMap target for an OBSERVABLE_INCLUDE instruction, but can't figure out its name.\n" + "(The name is used in order to give consistent index to OBSERVABLE_INCLUDE instructions.)\n" + "(The mapping is stored in the field `stimflow.ChunkBuilder.o2i`.)\n" + "\n" + "You can do either of the following to fix the error:\n" + " (a) Pass in a PauliMap with a name (see `stimflow.PauliMap.with_name(name)`)\n" + " (b) Do a manual override by adding `arg=index` to the `stimflow.ChunkBuilder.append` call\n" + "\n" + "Note that, if you do both (a) and (b), the builder will remember the " + "name-to-index association." + ) + elif arg is not None and targets.name is not None: + if not isinstance(arg, (int, float)) or arg != int(arg): + raise ValueError(f"{arg=} isn't an integer.") + old_arg = self.o2i.get(targets.name) + if old_arg is None: + self.o2i[targets.name] = int(arg) + elif old_arg != arg: + raise ValueError( + f"Specified {arg=} and {targets=} but {self.o2i[targets.name]=} is " + f"inconsistent with {arg=}." + ) + elif arg is None: + arg = self._ensure_obs_index_of(targets.name) + + self._ensure_indices( + targets.qubits, + context_gate=gate, + context_targets=targets, + context_arg=arg, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ) + ps = targets.to_stim_pauli_string(self.q2i) + self.circuit.append( + data.name, + [stim.target_pauli(q, ps[q]) for q in ps.pauli_indices()], + arg, + tag=tag, + ) + else: + t0 = self._num_measurements + times = self.lookup_mids(targets) + rec_targets = [stim.target_rec(t - t0) for t in sorted(times)] + self.circuit.append(data.name, rec_targets, arg, tag=tag) + + elif data.name == "MPP": + self._append_mpp( + gate=gate, + targets=cast(Any, targets), + arg=arg, + measure_key_func=cast(Any, measure_key_func), + tag=tag, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ) + + elif data.name == "E": + targets = list(targets) + if len(targets) != 1 or not isinstance(targets[0], PauliMap): + raise NotImplementedError( + "gate='CORRELATED_ERROR' " + "and len(targets) != 1 " + "and not isinstance(targets[0], stimflow.PauliMap)" + ) + if arg: + qs = sorted_complex(targets[0].keys()) + if self._ensure_indices( + qs, + context_gate=gate, + context_targets=targets, + context_arg=arg, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ): + stim_targets = [] + for q in qs: + i = self.q2i[q] + stim_targets.append(stim.target_pauli(i, targets[0][q])) + self.circuit.append("CORRELATED_ERROR", stim_targets, arg, tag=tag) + + elif data.is_two_qubit_gate: + self._append_2q( + gate=gate, + data=data, + targets=cast(Any, targets), + arg=arg, + measure_key_func=cast(Any, measure_key_func), + tag=tag, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ) + + elif data.is_single_qubit_gate: + self._append_1q( + gate=gate, + data=data, + targets=cast(Any, targets), + arg=arg, + measure_key_func=cast(Any, measure_key_func), + tag=tag, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ) + + else: + raise NotImplementedError(f"{gate=}") + + def _append_mpp( + self, + *, + gate: str, + targets: PauliMap | Tile | Iterable[PauliMap | Tile], + arg: float | Iterable[float] | None = None, + measure_key_func: Callable[[PauliMap | Tile], Any] | None, + tag: str, + unknown_qubit_append_mode: Literal["auto", "error", "skip", "include"], + ) -> None: + if not targets: + return + if arg == 0: + arg = None + if isinstance(targets, (PauliMap, Tile)): + raise ValueError( + f"{gate=} but {targets=} is a single stimflow.PauliMap instead of a list of " + f"stimflow.PauliMap." + ) + for target in targets: + if not isinstance(target, (PauliMap, Tile)): + raise ValueError(f"{gate=} but {target=} isn't a stimflow.PauliMap, or stimflow.Tile.") + + # Canonicalize qubit ordering of the pauli strings. + stim_targets = [] + for target in targets: + pauli_map: PauliMap = PauliMap(target) + if not pauli_map: + raise NotImplementedError(f"Attempted to measure empty pauli string {pauli_map=}.") + qs = sorted_complex(pauli_map) + if self._ensure_indices( + qs, + context_gate=gate, + context_targets=targets, + context_arg=arg, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ): + for q in qs: + i = self.q2i[q] + stim_targets.append(stim.target_pauli(i, pauli_map[q])) + stim_targets.append(stim.target_combiner()) + stim_targets.pop() + + self.circuit.append(gate, stim_targets, arg, tag=tag) + + for target in targets: + if measure_key_func is not None: + self._rec(measure_key_func(cast(Any, target)), [self._num_measurements]) + self._num_measurements += 1 + + def _append_1q( + self, + *, + gate: str, + data: stim.GateData, + targets: Iterable[complex], + arg: float | Iterable[float] | None, + measure_key_func: Callable[[complex], Any] | None, + tag: str, + unknown_qubit_append_mode: Literal["auto", "error", "skip", "include"], + ) -> None: + __tracebackhide__ = True + targets = tuple(targets) + self._ensure_indices( + targets, + context_gate=gate, + context_targets=targets, + context_arg=arg, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ) + indices: list[tuple[int, int]] = [] + for k in range(len(targets)): + i = self.q2i.get(targets[k]) + if i is not None: + indices.append((i, k)) + indices = sorted(indices) + if not indices: + return + + self.circuit.append(gate, [e[0] for e in indices], arg, tag=tag) + if data.produces_measurements: + for _, k in indices: + t = targets[k] + if measure_key_func is not None: + self._rec(measure_key_func(t), [self._num_measurements]) + self._num_measurements += 1 + + def _append_2q( + self, + *, + gate: str, + data: stim.GateData, + targets: Iterable[Sequence[complex]], + arg: float | Iterable[float] | None, + measure_key_func: Callable[[tuple[complex, complex]], Any] | None, + tag: str, + unknown_qubit_append_mode: Literal["auto", "error", "skip", "include"], + ) -> None: + __tracebackhide__ = True + + for target in targets: + if not hasattr(target, "__len__") or len(target) != 2: + raise ValueError( + f"{gate=} is a two-qubit gate, " + f"but {target=} isn't a pair of complex numbers." + ) + a, b = cast(Any, target) + self._ensure_indices( + (a, b), + context_gate=gate, + context_targets=targets, + context_arg=arg, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ) + + # Canonicalize gate and target pairs. + targets = [tuple(cast(Any, pair)) for pair in targets] + index_pairs: list[tuple[int, int, int]] = [] + index_swapped = data.name in _SWAP_CONJUGATED_MAP + index_sorted = data.is_symmetric_gate + for k in range(len(targets)): + a, b = targets[k] + ai = self.q2i.get(a) + bi = self.q2i.get(b) + if ai is not None and bi is not None: + if index_swapped or (index_sorted and ai > bi): + ai, bi = bi, ai + index_pairs.append((ai, bi, k)) + index_pairs = sorted(index_pairs) + if not index_pairs: + return + + if index_swapped: + gate = _SWAP_CONJUGATED_MAP[data.name] + + self.circuit.append(gate, [i for pair in index_pairs for i in pair[:2]], arg, tag=tag) + + # Record both qubit orderings. + if data.produces_measurements: + for _, _, k in index_pairs: + a, b = targets[k] + if measure_key_func is not None: + k1 = measure_key_func((a, b)) + k2 = measure_key_func((b, a)) + self._rec(k1, [self._num_measurements]) + if k1 != k2: + self._rec(k2, [self._num_measurements]) + self._num_measurements += 1 + + def append_feedback( + self, + *, + control_keys: Iterable[Any], + targets: Iterable[complex], + basis: str, + unknown_qubit_append_mode: Literal["auto", "error", "skip", "include"] = "auto", + ) -> None: + """Appends the tensor product of the given controls and targets into the circuit.""" + gate = f"C{basis}" + targets = tuple(targets) + self._ensure_indices( + targets, + context_targets=targets, + context_gate=f"classical C{basis}", + context_arg=None, + unknown_qubit_append_mode=unknown_qubit_append_mode, + ) + indices: list[int] = [] + for t in targets: + i = self.q2i.get(t) + if i is not None: + indices.append(i) + indices = sorted(indices) + t0 = self._num_measurements + times = self.lookup_mids(control_keys) + rec_targets = [stim.target_rec(t - t0) for t in sorted(times)] + for rec in rec_targets: + for i in indices: + self.circuit.append(gate, [rec, i]) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py new file mode 100644 index 00000000..80bb052d --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py @@ -0,0 +1,313 @@ +import numpy as np +import stim + +import stimflow + + +def test_builder_init(): + builder = stimflow.ChunkBuilder([0, 1j, 3 + 2j]) + assert builder.q2i == {0: 0, 1j: 1, 3 + 2j: 2} + assert builder.circuit == stim.Circuit( + """ + """ + ) + + +def test_append_tick(): + builder = stimflow.ChunkBuilder([0]) + builder.append("TICK") + builder.append("TICK") + assert builder.circuit == stim.Circuit( + """ + TICK + TICK + """ + ) + + +def test_append_shift_coords(): + builder = stimflow.ChunkBuilder([0]) + builder.append("SHIFT_COORDS", arg=[0, 0, 1]) + assert builder.circuit == stim.Circuit( + """ + SHIFT_COORDS(0, 0, 1) + """ + ) + + +def test_append_measurements(): + builder = stimflow.ChunkBuilder(range(6)) + + builder.append("MXX", [(2, 3)]) + assert builder.lookup_mids([(2, 3)]) == [0] + assert builder.lookup_mids([(3, 2)]) == [0] + + builder.append("MYY", [(5, 4)]) + assert builder.lookup_mids([(4, 5)]) == [1] + assert builder.lookup_mids([(5, 4)]) == [1] + + builder.append("M", [3]) + assert builder.lookup_mids([3]) == [2] + + +def test_append_measurements_canonical_order(): + builder = stimflow.ChunkBuilder(range(6)) + + builder.append("MX", [5, 2, 3]) + assert builder.lookup_mids([2]) == [0] + assert builder.lookup_mids([3]) == [1] + assert builder.lookup_mids([5]) == [2] + + builder.append("MZZ", [(5, 2), (3, 4)]) + assert builder.lookup_mids([(2, 5)]) == [3] + assert builder.lookup_mids([(3, 4)]) == [4] + + assert builder.circuit == stim.Circuit( + """ + MX 2 3 5 + MZZ 2 5 3 4 + """ + ) + + +def test_append_mpp(): + builder = stimflow.ChunkBuilder([2 + 3j, 5 + 7j, 11 + 13j]) + + xxx = stimflow.PauliMap.from_xs([2 + 3j, 5 + 7j, 11 + 13j]) + z_z = stimflow.PauliMap.from_zs([11 + 13j, 2 + 3j]) + builder.append("MPP", [xxx, z_z]) + assert builder.lookup_mids([xxx]) == [0] + assert builder.lookup_mids([z_z]) == [1] + + assert builder.circuit == stim.Circuit( + """ + MPP X0*X1*X2 Z0*Z2 + """ + ) + + +def test_append_observable_include(): + builder = stimflow.ChunkBuilder([2 + 3j, 5 + 7j, 11 + 13j]) + + builder.append("R", [5 + 7j]) + builder.append("M", [2 + 3j, 5 + 7j, 11 + 13j], measure_key_func=lambda e: (e, "X")) + builder.append("OBSERVABLE_INCLUDE", [(5 + 7j, "X")], arg=2) + + assert builder.circuit == stim.Circuit( + """ + R 1 + M 0 1 2 + OBSERVABLE_INCLUDE(2) rec[-2] + """ + ) + + +def test_append_detector(): + builder = stimflow.ChunkBuilder([2 + 3j, 5 + 7j, 11 + 13j]) + + builder.append("R", [5 + 7j]) + builder.append("M", [2 + 3j, 5 + 7j, 11 + 13j], measure_key_func=lambda e: (e, "X")) + builder.append("DETECTOR", [(5 + 7j, "X")], arg=[2, 3, 5]) + + assert builder.circuit == stim.Circuit( + """ + R 1 + M 0 1 2 + DETECTOR(2, 3, 5) rec[-2] + """ + ) + + +def test_make_surface_code_first_round(): + diameter = 3 + tiles = [] + + for x in range(-1, diameter): + for y in range(-1, diameter): + m = x + 1j * y + 0.5 + 0.5j + potential_data = [m + 1j**k * (0.5 + 0.5j) for k in range(4)] + data = [d for d in potential_data if 0 <= d.real < diameter if 0 <= d.imag < diameter] + if len(data) not in [2, 4]: + continue + + basis = "XZ"[(x.real + y.real) % 2 == 0] + if not (0 <= m.real < diameter - 1) and basis != "Z": + continue + if not (0 <= m.imag < diameter - 1) and basis != "X": + continue + tiles.append(stimflow.Tile(measure_qubit=m, data_qubits=data, bases=basis)) + + patch = stimflow.Patch(tiles) + obs_x = stimflow.PauliMap({q: "X" for q in patch.data_set if q.real == 0}).with_name("LX") + obs_z = stimflow.PauliMap({q: "Z" for q in patch.data_set if q.imag == 0}).with_name("LZ") + code = stimflow.StabilizerCode(patch, logicals=[(obs_x, obs_z)]).with_transformed_coords( + lambda e: e * (1 - 1j) + ) + + builder = stimflow.ChunkBuilder(code.used_set) + + mxs = {tile.measure_qubit for tile in code.patch if tile.basis == "X"} + mzs = {tile.measure_qubit for tile in code.patch if tile.basis == "Z"} + builder.append("RX", mxs) + builder.append("RZ", mzs | code.data_set) + builder.append("TICK") + + for layer in range(4): + offset = [1j, 1, -1, -1j][layer] + cxs = [] + for tile in code.tiles: + m = tile.measure_qubit + s = -1 if tile.basis == "Z" else +1 + d = m + offset * (s if 1 <= layer <= 2 else 1) + if d in code.data_set: + cxs.append((m, d)[::s]) + builder.append("CX", cxs) + builder.append("TICK") + builder.append("MX", mxs) + builder.append("MZ", mzs) + for z in stimflow.sorted_complex(mzs): + builder.append("DETECTOR", [z], arg=[z.real, z.imag, 0]) + + assert builder.circuit == stim.Circuit( + """ + RX 0 7 9 16 + R 1 2 3 4 5 6 8 10 11 12 13 14 15 + TICK + CX 0 1 4 3 7 8 9 10 12 11 14 13 + TICK + CX 0 2 1 3 6 11 7 12 8 13 9 14 + TICK + CX 7 2 8 3 9 4 10 5 15 13 16 14 + TICK + CX 2 3 4 5 7 6 9 8 12 13 16 15 + TICK + MX 0 7 9 16 + M 3 5 11 13 + DETECTOR(1, 0, 0) rec[-4] + DETECTOR(1, 2, 0) rec[-3] + DETECTOR(3, -2, 0) rec[-2] + DETECTOR(3, 0, 0) rec[-1] + """ + ) + + +def test_skip_unknown_1qm(): + builder = stimflow.ChunkBuilder(allowed_qubits=[0, 1, 2, 3]) + builder.append("M", [2, -1, 1, 25, 3], unknown_qubit_append_mode="skip") + assert builder.circuit == stim.Circuit( + """ + M 1 2 3 + """ + ) + assert builder.lookup_mids([1]) == [0] + assert builder.lookup_mids([2]) == [1] + assert builder.lookup_mids([3]) == [2] + + +def test_skip_unknown_2qm(): + builder = stimflow.ChunkBuilder(allowed_qubits=[0, 1, 2, 3]) + builder.append("MZZ", [(2, 3), (-1, 5), (0, 1)], unknown_qubit_append_mode="skip") + assert builder.q2i == {0: 0, 1: 1, 2: 2, 3: 3} + assert builder.circuit == stim.Circuit( + """ + MZZ 0 1 2 3 + """ + ) + assert builder.lookup_mids([(0, 1)]) == builder.lookup_mids([(1, 0)]) == [0] + assert builder.lookup_mids([(2, 3)]) == builder.lookup_mids([(3, 2)]) == [1] + + +def test_partial_observable_include_memory_experiment(): + obs_x = stimflow.PauliMap.from_xs([0, 1]).with_name("LX") + obs_z = stimflow.PauliMap.from_zs([0, 1j]).with_name("LZ") + stab_z0 = stimflow.PauliMap.from_zs([0, 1]) + stab_z1 = stimflow.PauliMap.from_zs([1j, 1 + 1j]) + stab_x = stimflow.PauliMap.from_xs([0, 1, 1j, 1 + 1j]) + + builder_init = stimflow.ChunkBuilder() + builder_init.append("R", [0, 1, 1j, 1 + 1j]) + builder_init.append("OBSERVABLE_INCLUDE", obs_x) + builder_init.add_flow(end=stab_z0) + builder_init.add_flow(end=stab_z1) + builder_init.add_flow(end=obs_z) + builder_init.add_flow(end=obs_x) + builder_init.add_discarded_flow_output(stab_x) + chunk_init = builder_init.finish_chunk() + + builder_bulk = stimflow.ChunkBuilder([0, 1, 1j, 1 + 1j]) + builder_bulk.append("MPP", [stab_z0]) + builder_bulk.append("MPP", [stab_z1]) + builder_bulk.append("MPP", [stab_x]) + + builder_bulk.add_flow(start=stab_z0, ms=[stab_z0]) + builder_bulk.add_flow(start=stab_z1, ms=[stab_z1]) + builder_bulk.add_flow(start=stab_x, ms=[stab_x]) + builder_bulk.add_flow(end=stab_z0, ms=[stab_z0]) + builder_bulk.add_flow(end=stab_z1, ms=[stab_z1]) + builder_bulk.add_flow(end=stab_x, ms=[stab_x]) + builder_bulk.add_flow(start=obs_x, end=obs_x) + builder_bulk.add_flow(start=obs_z, end=obs_z) + chunk_bulk = builder_bulk.finish_chunk() + + builder_end = stimflow.ChunkBuilder() + builder_end.append("OBSERVABLE_INCLUDE", obs_z) + builder_end.append("MX", [0, 1, 1j, 1 + 1j]) + builder_end.add_discarded_flow_input(stab_z0) + builder_end.add_discarded_flow_input(stab_z1) + builder_end.add_flow(start=obs_z) + builder_end.add_flow(start=stab_x, ms=stab_x.qubits) + builder_end.add_flow(start=obs_x, ms=obs_x.qubits) + chunk_end = builder_end.finish_chunk() + + chunk_init.verify() + chunk_bulk.verify() + chunk_end.verify() + + compiler = stimflow.ChunkCompiler() + compiler.append(chunk_init) + compiler.append(chunk_bulk * 3) + compiler.append(chunk_end) + + circuit = compiler.finish_circuit() + assert circuit == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1, 1) 3 + R 0 2 1 3 + OBSERVABLE_INCLUDE(0) X0 X2 + TICK + MPP Z0*Z2 Z1*Z3 X0*X1*X2*X3 + DETECTOR(0.5, 0, 0) rec[-3] + DETECTOR(0.5, 1, 0) rec[-2] + SHIFT_COORDS(0, 0, 1) + TICK + REPEAT 2 { + MPP Z0*Z2 Z1*Z3 X0*X1*X2*X3 + DETECTOR(0.5, 0, 0) rec[-6] rec[-3] + DETECTOR(0.5, 1, 0) rec[-5] rec[-2] + DETECTOR(0.5, 0.5, 0) rec[-4] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + } + OBSERVABLE_INCLUDE(1) Z0 Z1 + MX 0 1 2 3 + DETECTOR(0.5, 0.5, 0) rec[-5] rec[-4] rec[-3] rec[-2] rec[-1] + OBSERVABLE_INCLUDE(0) rec[-4] rec[-2] + """ + ) + + circuit.detector_error_model() # Check detector error model exists. + + # Check flip sampling behaves as expected. + dets, obs = circuit.compile_detector_sampler().sample(128, separate_observables=True) + assert np.count_nonzero(dets) == 0 + assert np.count_nonzero(obs) == 0 + + # Check measurement-to-obs conversion behaves as expected. + ms = circuit.compile_sampler().sample(512) + dets, obs = circuit.compile_m2d_converter().convert(measurements=ms, separate_observables=True) + assert np.count_nonzero(dets) == 0 + assert 200 <= np.count_nonzero(obs[:, 0]) <= 300 # Some measurements included in partial X obs + assert np.count_nonzero(obs[:, 1]) == 0 # No measurements included in partial Z obs diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py new file mode 100644 index 00000000..7a24b639 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py @@ -0,0 +1,656 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable, Iterable +from typing import Any, cast, Literal, TYPE_CHECKING + +import stim + +from stimflow._chunk._chunk import Chunk +from stimflow._chunk._chunk_interface import ChunkInterface +from stimflow._chunk._chunk_loop import ChunkLoop +from stimflow._chunk._chunk_reflow import ChunkReflow +from stimflow._chunk._flow_metadata import FlowMetadata +from stimflow._core import append_reindexed_content_to_circuit, Flow, PauliMap, sorted_complex + +if TYPE_CHECKING: + import stimflow + + +class ChunkCompiler: + """Compiles appended chunks into a unified circuit.""" + + def __init__(self, *, metadata_func: Callable[[Flow], FlowMetadata] | None = None): + """ + + Args: + metadata_func: Determines coordinate data appended to detectors + (after x, y, and t). Defaults to None (no extra metadata). + """ + if metadata_func is None: + metadata_func = lambda _: FlowMetadata() + self.open_flows: dict[PauliMap, Flow | Literal["discard"]] = {} + self.num_measurements: int = 0 + self.waiting_for_magic_init = False + self.circuit: stim.Circuit = stim.Circuit() + self.q2i: dict[complex, int] = {} + self.o2i: dict[Any, int] = {} + self.discarded_observables: set[int] = set() + self.metadata_func: Callable[[Flow], FlowMetadata] = cast(Any, metadata_func) + self.prev_chunk_wants_to_merge_with_next: bool = False + + def ensure_qubits_included(self, qubits: Iterable[complex]): + """Adds the given qubit positions to the indexed positions, if they aren't already.""" + for q in sorted_complex(qubits): + if q not in self.q2i: + self.q2i[q] = len(self.q2i) + + def ensure_observables_included(self, observable_names: Iterable[Any]): + for name in observable_names: + if name is not None and name not in self.o2i: + self.o2i[name] = len(self.o2i) + + def copy(self) -> ChunkCompiler: + """Returns a deep copy of the compiler's state.""" + result = ChunkCompiler(metadata_func=self.metadata_func) + result.open_flows = dict(self.open_flows) + result.num_measurements = self.num_measurements + result.circuit = self.circuit.copy() + result.q2i = dict(self.q2i) + result.o2i = dict(self.o2i) + result.discarded_observables = set(self.discarded_observables) + return result + + def __str__(self) -> str: + lines = ["ChunkCompiler {", " discard_flows {"] + + for key, flow in self.open_flows.items(): + if flow == "discard": + lines.append(f" {key}") + lines.append(" }") + + lines.append(" det_flows {") + for key, flow in self.open_flows.items(): + if isinstance(flow, Flow) and flow.obs_key is None: + lines.append(f" {flow.end}, ms={flow.measurement_indices}") + lines.append(" }") + + lines.append(" obs_flows {") + for key, flow in self.open_flows.items(): + if isinstance(flow, Flow) and flow.obs_key is not None: + lines.append(f" {flow.key_end}: ms={flow.measurement_indices}") + lines.append(" }") + + lines.append(f" num_measurements = {self.num_measurements}") + lines.append("}") + return "\n".join(lines) + + def cur_circuit_html_viewer(self) -> stimflow.str_html: + copy = self.copy() + if copy.open_flows: + copy.append_magic_end_chunk() + from stimflow._viz import stim_circuit_html_viewer + + return stim_circuit_html_viewer( + circuit=copy.finish_circuit(), background=self.cur_end_interface() + ) + + def finish_circuit(self) -> stim.Circuit: + """Returns the circuit built by the compiler. + + Performs some final translation steps: + - Re-indexing the qubits to be in a sorted order. + - Re-indexing the observables to omit discarded observable flows. + """ + + if self.open_flows or self.waiting_for_magic_init: + raise ValueError(f"Some flows were unterminated when finishing the circuit:\n\n{self}") + if len(set(self.q2i.values())) < len(self.q2i): + raise NotImplementedError( + "The qubit indexing map was inconsistent, probably due to a mix of manual and automatic indices." + ) + if len(set(self.o2i.values())) < len(self.o2i): + raise NotImplementedError( + "The observable indexing map was inconsistent, probably due to a mix of manual and automatic indices." + ) + + obs2i: dict[int, int | Literal["discard"]] = {} + next_obs_index = 0 + for obs_key, obs_index in sorted(self.o2i.items(), key=lambda e: e[1]): + if obs_key in self.discarded_observables: + obs2i[obs_index] = "discard" + elif isinstance(obs_key, int): + obs2i[obs_index] = next_obs_index + next_obs_index += 1 + for obs_key, obs_index in sorted(self.o2i.items(), key=lambda e: e[1]): + if obs_index not in obs2i: + obs2i[obs_index] = next_obs_index + next_obs_index += 1 + + new_q2i = {q: i for i, q in enumerate(sorted_complex(self.q2i.keys()))} + final_circuit = stim.Circuit() + for q, i in new_q2i.items(): + final_circuit.append("QUBIT_COORDS", [i], [q.real, q.imag]) + qubit2i: dict[int, int] = {i: new_q2i[q] for q, i in self.q2i.items()} + append_reindexed_content_to_circuit( + content=self.circuit, + out_circuit=final_circuit, + qubit_i2i=qubit2i, + obs_i2i=obs2i, + rewrite_detector_time_coordinates=False, + ) + while len(final_circuit) > 0 and ( + final_circuit[-1].name == "SHIFT_COORDS" or final_circuit[-1].name == "TICK" + ): + final_circuit.pop() + return final_circuit + + def append_magic_init_chunk(self, expected: ChunkInterface | None = None) -> None: + """Appends a non-physical chunk that outputs the flows expected by the next chunk. + + Args: + expected: Defaults to None (unused). If set to a ChunkInterface, it will be + verified that the next appended chunk actually has a start interface + matching the given expected interface. If set to None, then no checks are + performed; no constraints are placed on the next chunk. + """ + if expected is None: + self.waiting_for_magic_init = True + return + self.waiting_for_magic_init = False + + self.ensure_qubits_included(expected.used_set) + self.ensure_observables_included(port.name for port in sorted(expected.ports)) + self.ensure_observables_included(port.name for port in sorted(expected.discards)) + obs_ports = sorted(port for port in expected.ports if port.name is not None) + for obs_port in obs_ports: + assert obs_port not in self.open_flows + assert obs_port.name is not None + targets = [stim.target_pauli(self.q2i[q], p) for q, p in obs_port.items()] + self.open_flows[obs_port] = Flow(end=obs_port) + obs_index = self.o2i.setdefault(obs_port.name, len(self.o2i)) + metadata = self.metadata_func(Flow(obs_key=obs_port.name)) + self.circuit.append("OBSERVABLE_INCLUDE", targets, arg=obs_index, tag=metadata.tag) + self.circuit.append("TICK") + + for layer in expected.partitioned_detector_flows(): + for port in layer: + assert port not in self.open_flows + targets = [stim.target_pauli(self.q2i[q], p) for q, p in port.items()] + self.open_flows[port] = Flow(end=port, mids=[self.num_measurements]) + self.circuit.append("MPP", stim.target_combined_paulis(targets)) + self.num_measurements += 1 + self.circuit.append("TICK") + + def append_magic_end_chunk(self, expected: ChunkInterface | None = None) -> None: + """Appends a non-physical chunk that terminates the circuit, regardless of open flows. + + Args: + expected: Defaults to None (unused). If set to None, no extra checks are performed. + If set to a ChunkInterface, it is verified that the open flows actually + correspond to this interface. + """ + if self.waiting_for_magic_init: + self.waiting_for_magic_init = False + if expected is None: + expected = self.cur_end_interface() + self.ensure_qubits_included(expected.used_set) + self.ensure_observables_included(port.name for port in sorted(expected.ports)) + self.ensure_observables_included(port.name for port in sorted(expected.discards)) + obs_ports: list[PauliMap] = sorted(port for port in expected.ports if port.name is not None) + completed_flows = [] + for discarded in expected.discards: + v = self.open_flows.pop(discarded) + assert v == "discard" + for layer in expected.partitioned_detector_flows(): + if self.circuit and self.circuit[-1].name != "TICK": + skip = False + if self.circuit[-1].name == 'REPEAT': + body = self.circuit[-1].body_copy() + if body and body[-1].name == 'TICK': + skip = True + if not skip: + self.circuit.append("TICK") + for port in layer: + targets = [stim.target_pauli(self.q2i[q], p) for q, p in port.items()] + flow = self.open_flows.pop(port) + assert flow != "discard" + flow = cast(Flow, flow).fuse_with_next_flow( + Flow(start=port, mids=[self.num_measurements]), next_flow_measure_offset=0 + ) + self.circuit.append("MPP", stim.target_combined_paulis(targets)) + self.num_measurements += 1 + completed_flows.append(flow) + for obs_port in obs_ports: + if self.circuit and self.circuit[-1].name != "TICK": + self.circuit.append("TICK") + targets = [stim.target_pauli(self.q2i[q], p) for q, p in obs_port.items()] + flow = self.open_flows.pop(obs_port) + assert flow != "discard" + flow = cast(Flow, flow).fuse_with_next_flow( + Flow(start=obs_port), next_flow_measure_offset=0 + ) + obs_index = self.o2i.setdefault(obs_port.name, len(self.o2i)) + metadata = self.metadata_func(Flow(obs_key=obs_port.name)) + self.circuit.append("OBSERVABLE_INCLUDE", targets, arg=obs_index, tag=metadata.tag) + completed_flows.append(flow) + self._append_detectors(completed_flows=completed_flows) + + def cur_end_interface(self) -> ChunkInterface: + ports = [] + discards = [] + for pauli_map, flow in self.open_flows.items(): + if flow == "discard": + discards.append(pauli_map) + else: + ports.append(pauli_map) + return ChunkInterface(ports, discards=discards) + + def append(self, appended: Chunk | ChunkLoop | ChunkReflow) -> None: + """Appends a chunk to the circuit being built. + + The input flows of the appended chunk must exactly match the open outgoing flows of the + circuit so far. + """ + __tracebackhide__ = True + + if self.waiting_for_magic_init: + self.append_magic_init_chunk(appended.start_interface()) + + if isinstance(appended, Chunk): + self._append_chunk(chunk=appended) + elif isinstance(appended, ChunkReflow): + self._append_chunk_reflow(chunk_reflow=appended) + elif isinstance(appended, ChunkLoop): + self._append_chunk_loop(chunk_loop=appended) + else: + raise NotImplementedError(f"{appended=}") + + def _append_chunk_reflow(self, *, chunk_reflow: ChunkReflow) -> None: + for ps in chunk_reflow.discard_in: + if ps.name is not None: + self.discarded_observables.add(ps.name) + next_flows: dict[PauliMap, Flow | Literal["discard"]] = {} + for output, inputs in chunk_reflow.out2in.items(): + measurements: set[int] = set() + centers: list[complex] = [] + flags: set[str] = set() + discarded = False + for inp_key in inputs: + if inp_key not in self.open_flows: + msg = [f"Missing reflow input: {inp_key=}", "Needed inputs {"] + for ps in inputs: + msg.append(f" {ps}") + msg.append("}") + msg.append("Actual inputs {") + for ps in self.open_flows.keys(): + msg.append(f" {ps}") + msg.append("}") + raise ValueError("\n".join(msg)) + inp = self.open_flows[inp_key] + if inp == "discard": + discarded = True + else: + assert isinstance(inp, Flow) + assert not inp.start + measurements ^= frozenset(inp.measurement_indices) + if inp.center is not None: + centers.append(inp.center) + flags |= inp.flags + + next_flows[output] = ( + "discard" + if discarded + else Flow( + start=None, + end=output, + mids=tuple(sorted(measurements)), + flags=flags, + center=sum(centers) / len(centers) if centers else None, + ) + ) + + for k, v in self.open_flows.items(): + if k in chunk_reflow.removed_inputs: + continue + assert k not in next_flows + next_flows[k] = v + + self.open_flows = next_flows + + def _force_tick_separator(self, *, want_tick: bool) -> None: + if want_tick: + if ( + self.circuit + and self.circuit[-1].name != "TICK" + and self.circuit[-1].name != "REPEAT" + ): + self.circuit.append("TICK") + else: + # To make merging possible, break an iteration off the ending loop (if there is one). + while self.circuit and self.circuit[-1].name == "REPEAT": + block = self.circuit.pop() + body = block.body_copy() + if block.repeat_count > 1: + self.circuit.append( + stim.CircuitRepeatBlock(block.repeat_count - 1, body, tag=block.tag) + ) + self.circuit += body + if self.circuit and self.circuit[-1].name == "TICK": + self.circuit.pop() + + def _append_chunk(self, *, chunk: Chunk) -> None: + __tracebackhide__ = True + + # Index any new locations. + self.ensure_qubits_included(chunk.q2i.keys()) + self.ensure_observables_included(chunk.o2i.keys()) + + # Ensure chunks are correctly separated by a TICK. + self._force_tick_separator( + want_tick=not ( + self.prev_chunk_wants_to_merge_with_next or chunk.wants_to_merge_with_prev + ) + ) + self.prev_chunk_wants_to_merge_with_next = chunk.wants_to_merge_with_next + + # Attach new flows to existing flows. + next_flows, completed_flows = self._compute_next_flows(chunk=chunk) + for ps in chunk.discarded_inputs: + if ps.name is not None: + self.discarded_observables.add(ps.name) + for ps in chunk.discarded_outputs: + if ps.name is not None: + self.discarded_observables.add(ps.name) + self.num_measurements += chunk.circuit.num_measurements + self.open_flows = next_flows + + # Grow the compiled circuit. + qubit2i: dict[int, int] = {i: self.q2i[q] for q, i in chunk.q2i.items()} + obs2i: dict[int, int | Literal["discard"]] = { + i: self.o2i[name] for name, i in chunk.o2i.items() + } + append_reindexed_content_to_circuit( + content=chunk.circuit, + out_circuit=self.circuit, + qubit_i2i=qubit2i, + obs_i2i=obs2i, + rewrite_detector_time_coordinates=False, + ) + self._append_detectors(completed_flows=completed_flows) + + def _append_chunk_loop(self, *, chunk_loop: ChunkLoop) -> None: + __tracebackhide__ = True + past_circuit = self.circuit + + def compute_relative_flow_state(): + return { + k: ( + v.with_edits( + measurement_indices=[ + self._canonicalize_measurement_index_to_negative(m) + for m in v.measurement_indices + ] + ) + if isinstance(v, Flow) + else "discard" + ) + for k, v in self.open_flows.items() + } + + if self.prev_chunk_wants_to_merge_with_next: + if chunk_loop.repetitions > 0: + for chunk in chunk_loop.chunks: + self.append(chunk) + chunk_loop = chunk_loop.with_repetitions(chunk_loop.repetitions - 1) + + self._force_tick_separator(want_tick=True) + + iteration_circuits: list[stim.Circuit] = [] + measure_offset_start_of_loop = self.num_measurements + prev_rel_flow_state = compute_relative_flow_state() + while len(iteration_circuits) < chunk_loop.repetitions: + # Perform an iteration the hard way. + self.circuit = stim.Circuit() + for chunk in chunk_loop.chunks: + self.append(chunk) + self._force_tick_separator(want_tick=True) + iteration_circuits.append(self.circuit) + + # Check if we can fold the rest. + new_rel_flow_state = compute_relative_flow_state() + has_pre_loop_measurement = any( + m < measure_offset_start_of_loop + for flow in self.open_flows.values() + if isinstance(flow, Flow) + for m in flow.measurement_indices + ) + have_reached_steady_state = ( + not has_pre_loop_measurement and new_rel_flow_state == prev_rel_flow_state + ) + if have_reached_steady_state: + break + prev_rel_flow_state = new_rel_flow_state + + # Found a repeating iteration. + leftover_reps = chunk_loop.repetitions - len(iteration_circuits) + if leftover_reps > 0: + measurements_skipped = iteration_circuits[-1].num_measurements * leftover_reps + + # Fold identical repetitions at the end. + while len(iteration_circuits) > 1 and iteration_circuits[-1] == iteration_circuits[-2]: + leftover_reps += 1 + iteration_circuits.pop() + iteration_circuits[-1] *= leftover_reps + 1 + + self.num_measurements += measurements_skipped + self.open_flows = { + k: ( + v.with_edits( + measurement_indices=[ + m + measurements_skipped for m in v.measurement_indices + ] + ) + if isinstance(v, Flow) + else "discard" + ) + for k, v in self.open_flows.items() + } + + # Fuse iterations that happened to be equal. + self.circuit = past_circuit + if ( + self.circuit + and self.circuit[-1].name != "TICK" + and self.circuit[-1].name != "REPEAT" + and iteration_circuits + and iteration_circuits[0] + and iteration_circuits[0][0].name != "TICK" + ): + self.circuit.append("TICK") + k = 0 + while k < len(iteration_circuits): + k2 = k + 1 + while k2 < len(iteration_circuits) and iteration_circuits[k2] == iteration_circuits[k]: + k2 += 1 + self.circuit += iteration_circuits[k] * (k2 - k) + k = k2 + + def _canonicalize_measurement_index_to_negative(self, m: int) -> int: + if m >= 0: + m -= self.num_measurements + assert -self.num_measurements <= m < 0 + return m + + def _append_detectors(self, *, completed_flows: list[Flow]): + inserted_ops = stim.Circuit() + + # Dump observable changes. + for key, flow in list(self.open_flows.items()): + if key.name is not None and isinstance(flow, Flow) and flow.measurement_indices: + dump_targets: list[stim.GateTarget] = [] + for m in flow.measurement_indices: + dump_targets.append(stim.target_rec(m - self.num_measurements)) + obs_index = self.o2i.setdefault(flow.obs_key, len(self.o2i)) + inserted_ops.append("OBSERVABLE_INCLUDE", dump_targets, obs_index) + self.open_flows[key] = flow.with_edits(measurement_indices=[]) + + # Append detector and observable annotations for the completed flows. + detector_pos_usage_counts: collections.Counter[complex] = collections.Counter() + for flow in completed_flows: + rec_targets: list[stim.GateTarget] = [] + for m in flow.measurement_indices: + rec_targets.append( + stim.target_rec(self._canonicalize_measurement_index_to_negative(m)) + ) + metadata = self.metadata_func(flow) + if flow.obs_key is None: + dt = detector_pos_usage_counts[flow.center] + detector_pos_usage_counts[flow.center] += 1 + coords = (flow.center.real, flow.center.imag, dt, *metadata.extra_coords) + inserted_ops.append("DETECTOR", rec_targets, coords, tag=metadata.tag) + else: + obs_index = self.o2i.setdefault(flow.obs_key, len(self.o2i)) + if rec_targets: + if metadata.extra_coords: + raise ValueError( + f"{metadata=} for {flow=} has extra_coords, " + "but OBSERVABLE_INCLUDE instructions can't specify coordinates." + ) + inserted_ops.append( + "OBSERVABLE_INCLUDE", rec_targets, obs_index, tag=metadata.tag + ) + + if inserted_ops: + insert_index = len(self.circuit) + while insert_index > 0 and self.circuit[insert_index - 1].num_measurements == 0: + insert_index -= 1 + self.circuit.insert(insert_index, inserted_ops) + + # Shift the time coordinate so future chunks' detectors are further along the time axis. + det_offset = max(detector_pos_usage_counts.values(), default=0) + if det_offset > 0: + self.circuit.append("SHIFT_COORDS", [], (0, 0, det_offset)) + + def _compute_next_flows( + self, *, chunk: Chunk + ) -> tuple[dict[PauliMap, Flow | Literal["discard"]], list[Flow]]: + __tracebackhide__ = True + attached_flows, outgoing_discards = self._compute_attached_flows_and_discards(chunk=chunk) + + next_flows: dict[PauliMap, Flow | Literal["discard"]] = {} + completed_flows: list[Flow] = [] + for flow in attached_flows: + assert not flow.start + if flow.end: + next_flows[flow.end] = flow + else: + completed_flows.append(flow) + + for discarded in outgoing_discards: + next_flows[discarded] = "discard" + for discarded in chunk.discarded_outputs: + if discarded in next_flows: + raise ValueError( + f"Chunk said to discard {discarded=}, but it was already in next_flows." + ) + next_flows[discarded] = "discard" + + return next_flows, completed_flows + + def _compute_attached_flows_and_discards( + self, *, chunk: Chunk + ) -> tuple[list[Flow], list[PauliMap]]: + __tracebackhide__ = True + + result: list[Flow] = [] + old_flows = dict(self.open_flows) + + # Drop existing flows explicitly discarded by the chunk. + for discarded in chunk.discarded_inputs: + old_flows.pop(discarded, None) + outgoing_discards = [] + + # Attach the chunk's flows to the existing flows. + for new_flow in chunk.flows: + prev = old_flows.pop(new_flow.start, None) + if prev == "discard": + # Okay, discard it. + if new_flow.end: + outgoing_discards.append(new_flow.end) + elif isinstance(prev, Flow): + # Matched! Fuse them together. + result.append( + prev.fuse_with_next_flow( + new_flow, next_flow_measure_offset=self.num_measurements + ) + ) + elif not new_flow.start: + # Flow started inside the new chunk, so doesn't need to be matched. + result.append( + new_flow.with_edits( + measurement_indices=[ + (m + self.num_measurements if m >= 0 else m) + for m in new_flow.measurement_indices + ] + ) + ) + else: + # Failed to match. Describe the problem. + lines = [ + "A flow input wasn't satisfied.", + f" Expected input: {new_flow.start}", + " Available inputs:", + ] + for prev_avail in old_flows.keys(): + lines.append(f" {prev_avail}") + raise ValueError("\n".join(lines)) + + # Check for any unmatched flows. + dangling_flows: list[Flow] = [val for val in old_flows.values() if isinstance(val, Flow)] + if dangling_flows: + lines = ["Some flow outputs were unmatched when appending a new chunk:"] + for flow in dangling_flows: + lines.append(f" {flow.end}") + raise ValueError("\n".join(lines)) + + return result, outgoing_discards + + +def compile_chunks_into_circuit( + chunks: list[Chunk | ChunkLoop | ChunkReflow], + *, + use_magic_time_boundaries: bool = False, + metadata_func: Callable[[Flow], FlowMetadata] = lambda _: FlowMetadata(), +) -> stim.Circuit: + """Stitches together a series of chunks into a fault tolerant circuit. + + Args: + chunks: The sequence of chunks to compile into a circuit. + use_magic_time_boundaries: Defaults to False. When False, an error will be raised if the + first chunk has any non-empty input flows or the last chunk has any non-empty output + flows (indicating the circuit is not complete). When True, the compiler will + automatically close those flows by inserting MPP and OBSERVABLE_INCLUDE instructions to + explain the dangling flows. + metadata_func: Defaults to using no metadata. This function should take a stimflow.Flow and + return a stimflow.FlowMetadata. The metadata is used for adding tags to coordinates to + DETECTOR instructions and tags to DETECTOR/OBSERVABLE_INCLUDE instructions. + + Returns: + The compiled circuit. + """ + compiler = ChunkCompiler(metadata_func=metadata_func) + + if use_magic_time_boundaries and chunks: + compiler.append_magic_init_chunk() + + for k, chunk in enumerate(chunks): + try: + compiler.append(chunk) + except (ValueError, NotImplementedError) as ex: + raise type(ex)(f"Encountered error while appending chunk index {k}:\n{ex}") from ex + + if use_magic_time_boundaries and chunks: + compiler.append_magic_end_chunk() + + return compiler.finish_circuit() diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py new file mode 100644 index 00000000..7c5c5bae --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py @@ -0,0 +1,724 @@ +import stim + +import stimflow + + +def test_chunk_compiler_q2i(): + compiler = stimflow.ChunkCompiler() + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(2, 3) 0 + H 0 + """ + ), + flows=[], + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(2, 4) 0 + QUBIT_COORDS(2, 3) 1 + CX 0 1 + """ + ), + flows=[], + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(2, 4) 0 + S 0 + """ + ), + flows=[], + ) + ) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(2, 3) 0 + QUBIT_COORDS(2, 4) 1 + H 0 + TICK + CX 1 0 + TICK + S 1 + """ + ) + + +def test_chunk_compiler_single_flow(): + compiler = stimflow.ChunkCompiler() + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(1, 2) 0 + R 0 + """ + ), + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([1 + 2j]), center=3 + 5j)], + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(1, 2) 0 + M 0 + """ + ), + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([1 + 2j]), mids=[0], center=3 + 5j)], + ) + ) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(1, 2) 0 + R 0 + TICK + M 0 + DETECTOR(3, 5, 0) rec[-1] + """ + ) + + +def test_chunk_compiler_obs_flow_eager_dump(): + compiler = stimflow.ChunkCompiler() + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + """ + ), + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]), center=0, obs_key=0)], + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + MR 0 + """ + ), + flows=[ + stimflow.Flow( + start=stimflow.PauliMap.from_zs([0]), + end=stimflow.PauliMap.from_zs([0]), + mids=[0], + center=0, + obs_key=0, + ) + ], + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + M 0 + """ + ), + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), mids=[0], center=0, obs_key=0)], + ) + ) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + MR 0 + OBSERVABLE_INCLUDE(0) rec[-1] + TICK + M 0 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ) + + +def test_chunk_compiler_loop(): + compiler = stimflow.ChunkCompiler() + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(0, 2) 2 + QUBIT_COORDS(0, 3) 3 + R 0 1 2 3 + """ + ), + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([k]), center=0) for k in range(4)], + ) + ) + compiler.append( + stimflow.ChunkLoop( + [ + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(0, 2) 2 + QUBIT_COORDS(0, 3) 3 + SWAP 0 1 + SWAP 1 2 + SWAP 2 3 + M 3 + """ + ), + flows=[ + stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), mids=[0], center=0), + stimflow.Flow(end=stimflow.PauliMap.from_zs([3]), mids=[0], center=0), + stimflow.Flow( + start=stimflow.PauliMap.from_zs([1]), end=stimflow.PauliMap.from_zs([0]), center=0 + ), + stimflow.Flow( + start=stimflow.PauliMap.from_zs([2]), end=stimflow.PauliMap.from_zs([1]), center=0 + ), + stimflow.Flow( + start=stimflow.PauliMap.from_zs([3]), end=stimflow.PauliMap.from_zs([2]), center=0 + ), + ], + ) + ], + repetitions=1000, + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(0, 2) 2 + QUBIT_COORDS(0, 3) 3 + M 0 1 2 3 + """ + ), + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([k]), mids=[k], center=0) for k in range(4)], + ) + ) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(0, 2) 2 + QUBIT_COORDS(0, 3) 3 + R 0 1 2 3 + TICK + REPEAT 4 { + SWAP 0 1 1 2 2 3 + M 3 + DETECTOR(0, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + } + REPEAT 996 { + SWAP 0 1 1 2 2 3 + M 3 + DETECTOR(0, 0, 0) rec[-5] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + } + M 0 1 2 3 + DETECTOR(0, 0, 0) rec[-8] rec[-4] + DETECTOR(0, 0, 1) rec[-7] rec[-3] + DETECTOR(0, 0, 2) rec[-6] rec[-2] + DETECTOR(0, 0, 3) rec[-5] rec[-1] + """ + ) + + +def test_chunk_compiler_loop_obs(): + compiler = stimflow.ChunkCompiler() + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + """ + ), + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]), center=0, obs_key=3)], + ) + ) + compiler.append( + stimflow.ChunkLoop( + [ + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + MR 0 + """ + ), + flows=[ + stimflow.Flow( + start=stimflow.PauliMap.from_zs([0]), + end=stimflow.PauliMap.from_zs([0]), + mids=[0], + center=0, + obs_key=3, + ) + ], + ) + ], + repetitions=1000, + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + M 0 + """ + ), + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), mids=[0], center=0, obs_key=3)], + ) + ) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + REPEAT 1000 { + MR 0 + OBSERVABLE_INCLUDE(0) rec[-1] + TICK + } + M 0 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ) + + +def test_compile_postselected_chunks(): + chunk1 = stimflow.Chunk( + circuit=stim.Circuit( + """ + R 0 + """ + ), + q2i={0: 0}, + flows=[stimflow.Flow(center=0, end=stimflow.PauliMap({0: "Z"}))], + ) + chunk2 = stimflow.Chunk( + circuit=stim.Circuit( + """ + M 0 + """ + ), + q2i={0: 0}, + flows=[ + stimflow.Flow(center=0, end=stimflow.PauliMap({0: "Z"}), mids=[0]), + stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), mids=[0]), + ], + ) + chunk3 = stimflow.Chunk( + circuit=stim.Circuit( + """ + MR 0 + """ + ), + q2i={0: 0}, + flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), mids=[0])], + ) + + assert stimflow.compile_chunks_into_circuit([chunk1, chunk2, chunk3]).flattened() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + M 0 + DETECTOR(0, 0, 0) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1) rec[-2] rec[-1] + """ + ) + + assert stimflow.compile_chunks_into_circuit( + [ + chunk1.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk1.flows]), + chunk2, + chunk3, + ], + metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + ), + ).flattened() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + M 0 + DETECTOR(0, 0, 0, 999) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1) rec[-2] rec[-1] + """ + ) + + assert stimflow.compile_chunks_into_circuit( + [ + chunk1, + chunk2.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk2.flows]), + chunk3, + ], + metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + ), + ).flattened() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + M 0 + DETECTOR(0, 0, 0, 999) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1, 999) rec[-2] rec[-1] + """ + ) + + assert stimflow.compile_chunks_into_circuit( + [ + chunk1, + chunk2, + chunk3.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk3.flows]), + ], + metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + ), + ).flattened() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + M 0 + DETECTOR(0, 0, 0) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1, 999) rec[-2] rec[-1] + """ + ) + + assert stimflow.compile_chunks_into_circuit( + [ + chunk1, + chunk2.with_edits( + flows=[f.with_edits(flags={"postselect"}) if f.start else f for f in chunk2.flows] + ), + chunk3, + ], + metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + ), + ).flattened() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + M 0 + DETECTOR(0, 0, 0, 999) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1) rec[-2] rec[-1] + """ + ) + + +def test_chunk_compiler_propagate_discards(): + c = stimflow.ChunkCompiler() + xx = stimflow.PauliMap.from_xs([0, 1]) + zz = stimflow.PauliMap.from_zs([0, 1]) + c.append( + stimflow.Chunk( + stim.Circuit( + """ + R 0 1 + """ + ), + q2i={0: 0, 1: 1}, + flows=[stimflow.Flow(end=zz, center=0)], + discarded_outputs=[xx], + ) + ) + c.append( + stimflow.Chunk( + stim.Circuit( + """ + MZZ 0 1 + """ + ), + q2i={0: 0, 1: 1}, + flows=[ + stimflow.Flow(start=zz, center=0, mids=[0]), + stimflow.Flow(end=zz, center=0, mids=[0]), + stimflow.Flow(start=xx, end=xx, center=0), + ], + ) + ) + c.append( + stimflow.Chunk( + stim.Circuit( + """ + MX 0 1 + """ + ), + q2i={0: 0, 1: 1}, + discarded_inputs=[zz], + flows=[stimflow.Flow(start=xx, center=0, mids=[0])], + ) + ) + assert c.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + R 0 1 + TICK + MZZ 0 1 + DETECTOR(0, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + MX 0 1 + """ + ) + + +def test_drop_observable_later(): + c = stimflow.ChunkCompiler() + xx = stimflow.PauliMap.from_xs([0, 1]) + zz = stimflow.PauliMap.from_zs([0, 1]) + c.append( + stimflow.Chunk( + stim.Circuit( + """ + MPP X0*X1 + MPP Z0*Z1 + """ + ), + q2i={0: 0, 1: 1}, + flows=[ + stimflow.Flow(end=zz, obs_key="a", mids=[1]), + stimflow.Flow(end=xx, obs_key="b", mids=[0]), + ], + ) + ) + + c.append( + stimflow.Chunk( + stim.Circuit( + """ + MPP X0*X1 + """ + ), + q2i={0: 0, 1: 1}, + discarded_inputs=[zz.with_name("a")], + flows=[stimflow.Flow(start=xx, obs_key="b", mids=[0])], + ) + ) + + assert c.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + MPP X0*X1 Z0*Z1 + OBSERVABLE_INCLUDE(0) rec[-2] + TICK + MPP X0*X1 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ) + + +def test_chunk_negative_index_flow_measurement(): + chunk = stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + QUBIT_COORDS(2, 0) 2 + R 0 1 2 + M 0 1 2 + """ + ), + flows=[ + stimflow.Flow(mids=[-1], center=0), + stimflow.Flow(mids=[-2], center=0), + stimflow.Flow(mids=[-3], center=0), + ], + ) + compiler = stimflow.ChunkCompiler() + compiler.append(chunk) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + QUBIT_COORDS(2, 0) 2 + R 0 1 2 + M 0 1 2 + DETECTOR(0, 0, 0) rec[-1] + DETECTOR(0, 0, 1) rec[-2] + DETECTOR(0, 0, 2) rec[-3] + """ + ) + + +def test_merge_ticks(): + q2i = {k: k for k in range(3)} + init_chunk = stimflow.Chunk( + q2i=q2i, + circuit=stim.Circuit( + """ + R 0 2 + TICK + """ + ), + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0, 2]))], + wants_to_merge_with_next=True, + ) + rep_chunk = stimflow.Chunk( + q2i=q2i, + circuit=stim.Circuit( + """ + R 1 + TICK + CX 0 1 + TICK + CX 2 1 + TICK + M 1 + """ + ), + flows=[ + stimflow.Flow(start=stimflow.PauliMap.from_zs([0, 2]), mids=[0]), + stimflow.Flow(end=stimflow.PauliMap.from_zs([0, 2]), mids=[-1]), + ], + ) + measure_chunk = init_chunk.time_reversed() + + compiler = stimflow.ChunkCompiler() + compiler.append(init_chunk) + compiler.append(rep_chunk) + compiler.append(rep_chunk) + compiler.append(measure_chunk) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + QUBIT_COORDS(2, 0) 2 + R 0 2 1 + TICK + CX 0 1 + TICK + CX 2 1 + TICK + M 1 + DETECTOR(1, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + R 1 + TICK + CX 0 1 + TICK + CX 2 1 + TICK + M 1 + DETECTOR(1, 0, 0) rec[-2] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + M 2 0 + DETECTOR(1, 0, 0) rec[-3] rec[-2] rec[-1] + """ + ) + + +def test_preserves_tags(): + compiler = stimflow.ChunkCompiler() + builder = stimflow.ChunkBuilder() + builder.append("R", [0]) + builder.append("TICK") + builder.append("X", [0], tag="test") + builder.append("TICK") + builder.add_flow(end=stimflow.PauliMap({0: "Z"}), center=2) + compiler.append(builder.finish_chunk()) + compiler.append_magic_end_chunk() + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + TICK + X[test] 0 + TICK + MPP Z0 + DETECTOR(1, 0, 0) rec[-1] + """ + ) + + +def test_merges_with_loop(): + compiler = stimflow.ChunkCompiler() + s = stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + R 0 + """ + ), + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]))], + wants_to_merge_with_next=True, + ) + compiler.append(s) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 1) 0 + R 0 + M 0 + """ + ), + flows=[ + stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), end=stimflow.PauliMap.from_zs([0])), + stimflow.Flow(mids=[0]), + ], + ) + * 5 + ) + compiler.append(s.time_reversed()) + assert compiler.finish_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + R 0 1 + M 1 + DETECTOR(0, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + REPEAT 3 { + R 1 + M 1 + DETECTOR(0, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + } + R 1 + M 1 + DETECTOR(0, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + M 0 + DETECTOR(0, 0, 0) rec[-1] + """ + ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py b/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py new file mode 100644 index 00000000..729f1398 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import collections +import functools +from collections.abc import Callable, Iterable + +from stimflow._chunk._patch import Patch +from stimflow._chunk._stabilizer_code import StabilizerCode +from stimflow._core import PauliMap, str_svg, Tile + + +class ChunkInterface: + """Specifies a set of stabilizers and observables that a chunk can consume or prepare.""" + + def __init__(self, ports: Iterable[PauliMap], *, discards: Iterable[PauliMap] = ()): + self.ports = frozenset(ports) + self.discards = frozenset(discards) + + def partitioned_detector_flows(self) -> list[list[PauliMap]]: + """Returns the stabilizers of the interface, split into non-overlapping groups.""" + qubit_used: set[tuple[complex, int]] = set() + layers: collections.defaultdict[int, list[PauliMap]] = collections.defaultdict(list) + + for port in sorted(self.ports): + if port.name is None: + layer_index = 0 + while any((q, layer_index) in qubit_used for q in port.keys()): + layer_index += 1 + qubit_used.update((q, layer_index) for q in port.keys()) + layers[layer_index].append(port) + return [v for k, v in sorted(layers.items())] + + def with_transformed_coords(self, transform: Callable[[complex], complex]) -> ChunkInterface: + """Returns the same interface, but with coordinates transformed by the given function.""" + return ChunkInterface( + ports=[port.with_transformed_coords(transform) for port in self.ports], + discards=[discard.with_transformed_coords(transform) for discard in self.discards], + ) + + @functools.cached_property + def used_set(self) -> frozenset[complex]: + """Returns the set of qubits used in any flow mentioned by the chunk interface.""" + return frozenset(q for port in self.ports | self.discards for q in port.keys()) + + def to_svg( + self, + *, + show_order: bool = False, + show_measure_qubits: bool = False, + show_data_qubits: bool = True, + system_qubits: Iterable[complex] = (), + opacity: float = 1, + show_coords: bool = True, + show_obs: bool = True, + other: StabilizerCode | Patch | Iterable[StabilizerCode | Patch] | None = None, + tile_color_func: Callable[[Tile], str] | None = None, + rows: int | None = None, + cols: int | None = None, + find_logical_err_max_weight: int | None = None, + ) -> str_svg: + flat: list[StabilizerCode | Patch | ChunkInterface] = [self] + if isinstance(other, (StabilizerCode, Patch, ChunkInterface)): + flat.append(other) + elif other is not None: + flat.extend(other) + + from stimflow._viz import svg + + return svg( + objects=flat, + show_obs=show_obs, + show_measure_qubits=show_measure_qubits, + show_data_qubits=show_data_qubits, + show_order=show_order, + find_logical_err_max_weight=find_logical_err_max_weight, + system_qubits=system_qubits, + opacity=opacity, + show_coords=show_coords, + tile_color_func=tile_color_func, + cols=cols, + rows=rows, + ) + + def without_discards(self) -> ChunkInterface: + """Returns the same chunk interface, but with discarded flows not included.""" + return self.with_edits(discards=()) + + def without_keyed(self) -> ChunkInterface: + """Returns the same chunk interface, but without logical flows (named flows).""" + return ChunkInterface( + ports=[port for port in self.ports if port.name is None], + discards=[discard for discard in self.discards if discard.name is None], + ) + + def with_discards_as_ports(self) -> ChunkInterface: + """Returns the same chunk interface, but with discarded flows turned into normal flows.""" + return self.with_edits(discards=(), ports=self.ports | self.discards) + + def __repr__(self) -> str: + lines = ["stimflow.ChunkInterface("] + + lines.append(" ports=[") + for port in sorted(self.ports): + lines.append(f" {port!r},") + lines.append(" ],") + + if self.discards: + lines.append(" discards=[") + for discard in sorted(self.discards): + lines.append(f" {discard!r},") + lines.append(" ],") + + lines.append(")") + return "\n".join(lines) + + def __str__(self) -> str: + lines = [] + for port in sorted(self.ports): + lines.append(str(port)) + for discard in sorted(self.discards): + lines.append(f"discard {discard}") + return "\n".join(lines) + + def with_edits( + self, *, ports: Iterable[PauliMap] | None = None, discards: Iterable[PauliMap] | None = None + ) -> ChunkInterface: + """Returns an equivalent chunk interface but with the given values replaced.""" + return ChunkInterface( + ports=self.ports if ports is None else ports, + discards=self.discards if discards is None else discards, + ) + + def __eq__(self, other): + if not isinstance(other, ChunkInterface): + return NotImplemented + return self.ports == other.ports and self.discards == other.discards + + @functools.cached_property + def data_set(self) -> frozenset[complex]: + return frozenset( + q + for pauli_string_list in [self.ports, self.discards] + for ps in pauli_string_list + for q in ps.qubits + ) + + def to_patch(self) -> Patch: + """Returns a stimflow.Patch with tiles equal to the chunk interface's stabilizers.""" + return Patch( + tiles=[ + Tile(bases="".join(port.values()), data_qubits=port.keys(), measure_qubit=None) + for pauli_string_list in [self.ports, self.discards] + for port in pauli_string_list + if port.name is None + ] + ) + + def to_code(self) -> StabilizerCode: + """Returns a stimflow.StabilizerCode with an equivalent interface.""" + return StabilizerCode( + stabilizers=self.to_patch(), + logicals=[ + port + for pauli_string_list in [self.ports, self.discards] + for port in pauli_string_list + if port.name is not None + ], + ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py b/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py new file mode 100644 index 00000000..c40b3b70 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING + +import stim + +from stimflow._chunk._code_util import ( + verify_distance_is_at_least_2, + verify_distance_is_at_least_3, +) +from stimflow._chunk._patch import Patch +from stimflow._chunk._stabilizer_code import StabilizerCode +from stimflow._core import NoiseModel, str_html, Tile + +if TYPE_CHECKING: + from stimflow._chunk._chunk import Chunk + from stimflow._chunk._chunk_interface import ChunkInterface + from stimflow._chunk._chunk_reflow import ChunkReflow + + +class ChunkLoop: + """Specifies a series of chunks to repeat a fixed number of times. + + The loop invariant is that the last chunk's end interface should match the + first chunk's start interface (unless the number of repetitions is less than + 2). + + For duck typing purposes, many methods supported by Chunk are supported by + ChunkLoop. + """ + + def __init__(self, chunks: Iterable[Chunk | ChunkLoop], repetitions: int): + self.chunks = tuple(chunks) + self.repetitions = repetitions + + def start_interface(self) -> ChunkInterface: + """Returns the start interface of the first chunk in the loop.""" + return self.chunks[0].start_interface() + + def end_interface(self) -> ChunkInterface: + """Returns the end interface of the last chunk in the loop.""" + return self.chunks[-1].end_interface() + + def verify( + self, + *, + expected_in: ChunkInterface | None = None, + expected_out: ChunkInterface | None = None, + ): + expected_ins: list[ChunkInterface | None] = [c.end_interface() for c in self.chunks] + expected_ins = expected_ins[-1:] + expected_ins[:-1] + + expected_outs: list[ChunkInterface | None] = [c.start_interface() for c in self.chunks] + expected_outs = expected_outs[1:] + expected_outs[:1] + + if self.repetitions == 1: + expected_ins[0] = None + expected_outs[-1] = None + if expected_in is not None: + expected_ins[0] = expected_in + if expected_out is not None: + expected_outs[-1] = expected_out + for k, (chunk, inp, out) in enumerate(zip(self.chunks, expected_ins, expected_outs)): + try: + chunk.verify(expected_in=inp, expected_out=out) + except (AssertionError, ValueError) as ex: + raise ValueError(f"ChunkLoop failed to verify at sub-chunk index {k}") from ex + + def __mul__(self, other: int) -> ChunkLoop: + return self.with_repetitions(other * self.repetitions) + + def time_reversed(self) -> ChunkLoop: + """Returns the same loop, but time reversed. + + The time reversed loop has reversed flows, implemented by performs operations in the + reverse order and exchange measurements for resets (and vice versa) as appropriate. + It has exactly the same fault tolerant structure, just mirrored in time. + """ + rev_chunks = [chunk.time_reversed() for chunk in self.chunks[::-1]] + return ChunkLoop(rev_chunks, self.repetitions) + + def with_repetitions(self, new_repetitions: int) -> ChunkLoop: + """Returns the same loop, but with a different number of repetitions.""" + return ChunkLoop(chunks=self.chunks, repetitions=new_repetitions) + + def start_patch(self) -> Patch: + return self.chunks[0].start_patch() + + def end_patch(self) -> Patch: + return self.chunks[-1].end_patch() + + def flattened(self) -> list[Chunk | ChunkReflow]: + """Unrolls the loop, and any sub-loops, into a series of chunks.""" + return [e for c in self.chunks for e in c.flattened()] + + def find_distance( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 1e-3, + noiseless_qubits: Iterable[float | int | complex] = (), + ) -> int: + err = self.find_logical_error( + max_search_weight=max_search_weight, noise=noise, noiseless_qubits=noiseless_qubits + ) + return len(err) + + def to_closed_circuit(self) -> stim.Circuit: + """Compiles the chunk into a circuit by conjugating with mpp init/end chunks.""" + from stimflow._chunk._chunk_compiler import ChunkCompiler + + compiler = ChunkCompiler() + compiler.append_magic_init_chunk() + compiler.append(self) + compiler.append_magic_end_chunk() + return compiler.finish_circuit() + + def verify_distance_is_at_least_2(self, *, noise: float | NoiseModel = 1e-3): + """Verifies undetected logical errors require at least 2 physical errors. + + Verifies using a uniform depolarizing circuit noise model. + """ + __tracebackhide__ = True + circuit = self.to_closed_circuit() + if isinstance(noise, float): + noise = NoiseModel.uniform_depolarizing(1e-3) + circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) + verify_distance_is_at_least_2(circuit) + + def verify_distance_is_at_least_3(self, *, noise: float | NoiseModel = 1e-3): + """Verifies undetected logical errors require at least 3 physical errors. + + By default, verifies using a uniform depolarizing circuit noise model. + """ + __tracebackhide__ = True + circuit = self.to_closed_circuit() + if isinstance(noise, float): + noise = NoiseModel.uniform_depolarizing(1e-3) + circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) + verify_distance_is_at_least_3(circuit) + + def find_logical_error( + self, + *, + max_search_weight: int, + noise: float | NoiseModel = 1e-3, + noiseless_qubits: Iterable[float | int | complex] = (), + ) -> list[stim.ExplainedError]: + """Searches for a minium distance undetected logical error. + + By default, searches using a uniform depolarizing circuit noise model. + """ + circuit = self.to_closed_circuit() + if isinstance(noise, float): + noise = NoiseModel.uniform_depolarizing(1e-3) + circuit = noise.noisy_circuit_skipping_mpp_boundaries( + circuit, immune_qubit_coords=noiseless_qubits + ) + if max_search_weight == 2: + return circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) + return circuit.search_for_undetectable_logical_errors( + dont_explore_edges_with_degree_above=max_search_weight, + dont_explore_detection_event_sets_with_size_above=max_search_weight, + dont_explore_edges_increasing_symptom_degree=False, + canonicalize_circuit_errors=True, + ) + + def to_html_viewer( + self, + *, + patch: Patch | StabilizerCode | ChunkInterface | None = None, + tile_color_func: Callable[[Tile], tuple[float, float, float, float]] | None = None, + known_error: Iterable[stim.ExplainedError] | None = None, + ) -> str_html: + """Returns an HTML document containing a viewer for the chunk loop's circuit.""" + from stimflow._viz import stim_circuit_html_viewer + + if patch is None: + patch = self.start_patch() + if len(patch.tiles) == 0: + patch = self.end_patch() + return stim_circuit_html_viewer( + self.to_closed_circuit(), + background=patch, + tile_color_func=tile_color_func, + known_error=known_error, + ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py new file mode 100644 index 00000000..08550528 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py @@ -0,0 +1,309 @@ +from __future__ import annotations + +import functools +from collections.abc import Callable, Iterable +from typing import Any, cast, Literal, TYPE_CHECKING + +from stimflow._chunk._patch import Patch +from stimflow._chunk._stabilizer_code import StabilizerCode +from stimflow._chunk._test_util import assert_has_same_set_of_items_as +from stimflow._core import PauliMap, sorted_complex, Tile + +if TYPE_CHECKING: + from stimflow._chunk._chunk_interface import ChunkInterface + + +class ChunkReflow: + """An adapter chunk for attaching chunks describing the same thing in different ways. + + For example, consider two surface code idle round chunks where one has the logical + operator on the left side and the other has the logical operator on the right side. + They can't be directly concatenated, because their flows don't match. But a reflow + chunk can be placed in between, mapping the left logical operator to the right + logical operator times a set of stabilizers, in order to bridge the incompatibility. + """ + + def __init__(self, out2in: dict[PauliMap, list[PauliMap]], discard_in: Iterable[PauliMap] = ()): + self.out2in = out2in + self.discard_in = tuple(discard_in) + assert isinstance(self.out2in, dict) + for k, vs in self.out2in.items(): + assert isinstance(k, PauliMap), k + assert isinstance(vs, list) + for v in vs: + assert isinstance(v, PauliMap) + + @staticmethod + def from_auto_rewrite( + *, inputs: Iterable[PauliMap], out2in: dict[PauliMap, list[PauliMap] | Literal["auto"]] + ) -> ChunkReflow: + new_out2in: dict[PauliMap, list[PauliMap]] = {} + unsolved: list[PauliMap] = [] + for pk, pv in out2in.items(): + if pv == "auto": + unsolved.append(pk) + else: + new_out2in[pk] = cast(Any, pv) + if not unsolved: + return ChunkReflow(out2in=new_out2in) + + rows: list[tuple[set[int], PauliMap]] = [] + inputs = list(inputs) + qs: set[complex] = set() + for index in range(len(inputs)): + rows.append(({index}, inputs[index])) + qs |= inputs[index].keys() + for pv2 in unsolved: + rows.append((set(), pv2)) + num_solved = 0 + for q in sorted_complex(qs): + for b in "ZX": + for pivot in range(num_solved, len(inputs)): + p = rows[pivot][1][q] + if p != b and p != "I": + break + else: + continue + for row in range(len(rows)): + p = rows[row][1][q] + if row != pivot and p != b and p != "I": + a1, b1 = rows[row] + a2, b2 = rows[pivot] + rows[row] = (a1 ^ a2, b1 * b2) + if pivot != num_solved: + rows[num_solved], rows[pivot] = rows[pivot], rows[num_solved] + num_solved += 1 + for index in range(len(unsolved)): + v = rows[index + len(inputs)] + if v[1]: + raise ValueError(f"Failed to solve for {unsolved[index]}.") + new_out2in[unsolved[index]] = [inputs[v2] for v2 in v[0]] + + return ChunkReflow(out2in=new_out2in) + + @staticmethod + def from_auto_rewrite_transitions_using_stable( + *, stable: Iterable[PauliMap], transitions: Iterable[tuple[PauliMap, PauliMap]] + ) -> ChunkReflow: + """Bridges the given transitions using products from the given stable values.""" + new_out2in: dict[PauliMap, list[PauliMap]] = {} + + stable = list(stable) + rows: list[tuple[set[int], PauliMap]] = [] + used_qubits: set[complex] = set() + for index, s in enumerate(stable): + new_out2in[s] = [s] + used_qubits |= s.keys() + rows.append(({index}, s)) + num_stable_rows = len(rows) + + unsolved: list[tuple[PauliMap, PauliMap]] = [] + for inp, out in transitions: + assert inp.name == out.name + if inp == out: + new_out2in[out] = [inp] + else: + unsolved.append((inp, out)) + if not unsolved: + return ChunkReflow(out2in=new_out2in) + + for inp, out in unsolved: + rows.append((set(), PauliMap(inp) * PauliMap(out))) + num_solved = 0 + for q in sorted_complex(used_qubits): + for b in "ZX": + for pivot in range(num_solved, num_stable_rows): + p = rows[pivot][1][q] + if p != b and p != "I": + break + else: + continue + for row in range(len(rows)): + p = rows[row][1][q] + if row != pivot and p != b and p != "I": + a1, b1 = rows[row] + a2, b2 = rows[pivot] + rows[row] = (a1 ^ a2, b1 * b2) + if pivot != num_solved: + rows[num_solved], rows[pivot] = rows[pivot], rows[num_solved] + num_solved += 1 + for index in range(len(unsolved)): + inp, out = unsolved[index] + used_indices, remainder = rows[index + num_stable_rows] + if remainder: + raise ValueError(f"Failed to solve for {inp} -> {out}.") + new_out2in[out] = [inp] + [stable[k] for k in used_indices] + + return ChunkReflow(out2in=new_out2in) + + def with_obs_flows_as_det_flows(self): + return ChunkReflow( + out2in={PauliMap(k): [PauliMap(v) for v in vs] for k, vs in self.out2in.items()}, + discard_in=[PauliMap(k) for k in self.discard_in], + ) + + def with_transformed_coords(self, transform: Callable[[complex], complex]) -> ChunkReflow: + return ChunkReflow( + out2in={ + kp.with_transformed_coords(transform): [ + vp.with_transformed_coords(transform) for vp in vs + ] + for kp, vs in self.out2in.items() + }, + discard_in=[kp.with_transformed_coords(transform) for kp in self.discard_in], + ) + + def start_interface(self) -> ChunkInterface: + from stimflow._chunk._chunk_interface import ChunkInterface + + return ChunkInterface( + ports={v for vs in self.out2in.values() for v in vs}, discards=self.discard_in + ) + + def end_interface(self) -> ChunkInterface: + from stimflow._chunk._chunk_interface import ChunkInterface + + return ChunkInterface(ports=self.out2in.keys(), discards=self.discard_in) + + def start_code(self) -> StabilizerCode: + tiles: list[Tile] = [] + observables: list[PauliMap] = [] + for obs in self.removed_inputs: + if obs.name is None: + tiles.append(Tile(data_qubits=obs.keys(), bases="".join(obs.values()))) + else: + observables.append(obs) + return StabilizerCode(stabilizers=Patch(tiles), logicals=observables) + + def start_patch(self) -> Patch: + return self.start_code().patch + + def end_code(self) -> StabilizerCode: + tiles: list[Tile] = [] + observables: list[PauliMap] = [] + for obs in self.out2in.keys(): + if obs.name is None: + tiles.append(Tile(data_qubits=obs.keys(), bases="".join(obs.values()))) + else: + observables.append(obs) + return StabilizerCode(stabilizers=Patch(tiles), logicals=observables) + + def end_patch(self) -> Patch: + return self.end_code().patch + + @functools.cached_property + def removed_inputs(self) -> frozenset[PauliMap]: + return frozenset(v for vs in self.out2in.values() for v in vs) | frozenset(self.discard_in) + + def verify( + self, + *, + expected_in: StabilizerCode | ChunkInterface | None = None, + expected_out: StabilizerCode | ChunkInterface | None = None, + ): + """Verifies that the ChunkReflow is well-formed.""" + from stimflow._chunk._chunk_interface import ChunkInterface + + assert isinstance(self.out2in, dict) + for k, vs in self.out2in.items(): + assert isinstance(k, PauliMap), k + assert isinstance(vs, list) + for v in vs: + assert isinstance(v, PauliMap) + + for k, vs in self.out2in.items(): + acc = PauliMap({}) + for v in vs: + acc *= PauliMap(v) + if acc != PauliMap(k): + lines = [ + "A reflow output wasn't equal to the product of its inputs.", + f" Output: {k}", + f" Difference: {PauliMap(k) * acc}", + " Inputs:", + ] + for v in vs: + lines.append(f" {v}") + raise ValueError("\n".join(lines)) + + if expected_in is not None: + if isinstance(expected_in, StabilizerCode): + expected_in = expected_in.as_interface() + assert isinstance(expected_in, ChunkInterface) + assert_has_same_set_of_items_as( + self.start_interface().with_discards_as_ports().ports, + expected_in.with_discards_as_ports().ports, + "self.start_interface().with_discards_as_ports().ports", + "expected_in.with_discards_as_ports().ports", + ) + + if expected_out is not None: + if isinstance(expected_out, StabilizerCode): + expected_out = expected_out.as_interface() + assert isinstance(expected_out, ChunkInterface) + assert_has_same_set_of_items_as( + self.end_interface().with_discards_as_ports().ports, + expected_out.with_discards_as_ports().ports, + "self.end_interface().with_discards_as_ports().ports", + "expected_out.with_discards_as_ports().ports", + ) + + if len(self.out2in) != len(self.removed_inputs): + msg = ["Number of outputs != number of distinct inputs.", "Outputs {"] + for ps, obs in self.out2in: + msg.append(f" {ps}, obs={obs}") + msg.append("}") + msg.append("Distinct inputs {") + for ps, obs in self.removed_inputs: + msg.append(f" {ps}, obs={obs}") + msg.append("}") + raise ValueError("\n".join(msg)) + + def __eq__(self, other) -> bool: + if isinstance(other, ChunkReflow): + kv1 = {k: set(v) for k, v in self.out2in.items()} + kv2 = {k: set(v) for k, v in other.out2in.items()} + return kv1 == kv2 and self.discard_in == other.discard_in + return False + + def __ne__(self, other) -> bool: + return not (self == other) + + def __repr__(self) -> str: + lines = [] + lines.append("stimflow.ChunkReflow(") + lines.append(" out2in={") + for k, v in self.out2in.items(): + if len(v) == 1 and v[0] == k: + lines.append(f" {k!r}: {v!r},") + else: + lines.append(f" {k!r}: [") + for v2 in v: + lines.append(f" {v2!r},") + lines.append(" ],") + lines.append(" },") + lines.append(" discard_in=(") + for discarded_in in self.discard_in: + lines.append(f" {discarded_in!r},") + lines.append(" ),") + return "\n".join(lines) + + def __str__(self) -> str: + lines = ["Reflow {"] + for k, v in self.out2in.items(): + if [k] != v: + lines.append(f" gather {k} {{") + for v2 in v: + lines.append(f" {v2}") + lines.append(" }") + for k, v in self.out2in.items(): + if [k] == v: + lines.append(f" keep {k}") + for k in self.discard_in: + lines.append(f" discard {k}") + lines.append("}") + return "\n".join(lines) + + def flattened(self) -> list[ChunkReflow]: + """This is here for duck-type compatibility with ChunkLoop.""" + return [self] diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py new file mode 100644 index 00000000..438c42df --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py @@ -0,0 +1,71 @@ +import stimflow + + +def test_from_auto_rewrite_xs(): + result = stimflow.ChunkReflow.from_auto_rewrite( + inputs=[ + stimflow.PauliMap({"X": [2, 3]}), + stimflow.PauliMap({"X": [3, 4]}), + stimflow.PauliMap({"X": [4, 5, 6]}), + stimflow.PauliMap({"X": [5, 7]}), + stimflow.PauliMap({"X": [8, 6]}), + stimflow.PauliMap({"X": [7, 6]}), + ], + out2in={ + stimflow.PauliMap({"X": [2, 3]}): [stimflow.PauliMap({"X": [2, 3]})], + stimflow.PauliMap({"X": [2]}): "auto", + }, + ) + assert result == stimflow.ChunkReflow( + out2in={ + stimflow.PauliMap({"X": [2, 3]}): [stimflow.PauliMap({"X": [2, 3]})], + stimflow.PauliMap({"X": [2]}): [ + stimflow.PauliMap({"X": [2, 3]}), + stimflow.PauliMap({"X": [3, 4]}), + stimflow.PauliMap({"X": [4, 5, 6]}), + stimflow.PauliMap({"X": [5, 7]}), + stimflow.PauliMap({"X": [7, 6]}), + ], + } + ) + + +def test_from_auto_rewrite_xyz(): + result = stimflow.ChunkReflow.from_auto_rewrite( + inputs=[stimflow.PauliMap({"X": [2, 3]}), stimflow.PauliMap({"Z": [2, 3]})], + out2in={stimflow.PauliMap({"Y": [2, 3]}): "auto"}, + ) + assert result == stimflow.ChunkReflow( + out2in={ + stimflow.PauliMap({"Y": [2, 3]}): [stimflow.PauliMap({"X": [2, 3]}), stimflow.PauliMap({"Z": [2, 3]})] + } + ) + + +def test_from_auto_rewrite_keyed(): + result = stimflow.ChunkReflow.from_auto_rewrite( + inputs=[stimflow.PauliMap({"X": [2, 3]}), stimflow.PauliMap({"Z": [2, 3]}).with_name("test")], + out2in={stimflow.PauliMap({"Y": [2, 3]}): "auto"}, + ) + assert result == stimflow.ChunkReflow( + out2in={ + stimflow.PauliMap({"Y": [2, 3]}): [ + stimflow.PauliMap({"X": [2, 3]}), + stimflow.PauliMap({"Z": [2, 3]}).with_name("test"), + ] + } + ) + + +def test_from_auto_rewrite_transitions_using_stable(): + x12 = stimflow.PauliMap.from_xs([1, 2]) + y12 = stimflow.PauliMap.from_ys([1, 2]) + z12 = stimflow.PauliMap.from_zs([1, 2]) + x1 = stimflow.PauliMap.from_xs([1]) + x2 = stimflow.PauliMap.from_xs([2]) + assert stimflow.ChunkReflow.from_auto_rewrite_transitions_using_stable( + stable=[x12], transitions=[(x1, x2)] + ) == stimflow.ChunkReflow(out2in={x12: [x12], x2: [x12, x1]}) + assert stimflow.ChunkReflow.from_auto_rewrite_transitions_using_stable( + stable=[y12], transitions=[(z12.with_name("test"), x12.with_name("test"))] + ) == stimflow.ChunkReflow(out2in={y12: [y12], x12.with_name("test"): [y12, z12.with_name("test")]}) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py new file mode 100644 index 00000000..4d63d733 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py @@ -0,0 +1,551 @@ +import stim + +import stimflow + + +def test_inverse_flows(): + chunk = stimflow.Chunk( + circuit=stim.Circuit( + """ + R 0 1 2 3 4 + CX 2 0 + M 0 + """ + ), + q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, + flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({}), mids=[0], end=stimflow.PauliMap({1: "Z"}))], + ) + + inverted = chunk.time_reversed() + inverted.verify() + assert len(inverted.flows) == len(chunk.flows) + assert inverted.circuit == stim.Circuit( + """ + R 0 + CX 2 0 + M 4 3 2 1 0 + """ + ) + + +def test_inverse_circuit(): + chunk = stimflow.Chunk( + circuit=stim.Circuit( + """ + R 0 1 2 3 4 + CX 2 0 3 4 + X 1 + M 0 + """ + ), + q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, + flows=[], + ) + + inverted = chunk.time_reversed() + inverted.verify() + assert len(inverted.flows) == len(chunk.flows) + assert inverted.circuit == stim.Circuit( + """ + M 0 + X 1 + CX 3 4 2 0 + M 4 3 2 1 0 + """ + ) + + +def test_reflow(): + xx = stimflow.PauliMap({0: "X", 1: "X"}) + yy = stimflow.PauliMap({0: "Y", 1: "Y"}) + zz = stimflow.PauliMap({0: "Z", 1: "Z"}) + chunk1 = stimflow.Chunk( + q2i={0: 0, 1: 1}, + circuit=stim.Circuit( + """ + MPP X0*X1 + MPP Z0*Z1 + """ + ), + flows=[stimflow.Flow(end=xx, mids=[0], center=0), stimflow.Flow(end=zz, mids=[1], center=0)], + ) + chunk2 = stimflow.Chunk( + q2i={0: 0, 1: 1}, + circuit=stim.Circuit( + """ + MPP Y0*Y1 + """ + ), + flows=[stimflow.Flow(start=yy, mids=[0], center=0)], + discarded_inputs=[xx], + ) + reflow = stimflow.ChunkReflow({yy: [xx, zz], xx: [xx]}) + chunk1.verify() + chunk2.verify() + reflow.verify() + stimflow.compile_chunks_into_circuit([chunk1, reflow, chunk2]) + + +def test_from_circuit_with_mpp_boundaries_simple(): + chunk = stimflow.Chunk.from_circuit_with_mpp_boundaries( + stim.Circuit( + """ + QUBIT_COORDS(1, 2) 0 + MPP X0 + H 0 + MPP Z0 + DETECTOR rec[-1] rec[-2] + """ + ) + ) + chunk.verify() + assert chunk == stimflow.Chunk( + q2i={1 + 2j: 0}, + flows=[ + stimflow.Flow( + start=stimflow.PauliMap.from_xs([1 + 2j]), + end=stimflow.PauliMap.from_zs([1 + 2j]), + mids=(), + obs_key=None, + center=1 + 2j, + ) + ], + circuit=stim.Circuit( + """ + H 0 + """ + ), + ) + + chunk = stimflow.Chunk.from_circuit_with_mpp_boundaries( + stim.Circuit( + """ + QUBIT_COORDS(1, 2) 0 + MR 0 + MR 0 + DETECTOR rec[-1] + """ + ) + ) + chunk.verify() + assert chunk == stimflow.Chunk( + q2i={1 + 2j: 0}, + flows=[], + circuit=stim.Circuit( + """ + MR 0 + MR 0 + DETECTOR rec[-1] + """ + ), + ) + + chunk = stimflow.Chunk.from_circuit_with_mpp_boundaries( + stim.Circuit( + """ + QUBIT_COORDS(1, 2) 0 + QUBIT_COORDS(1, 3) 1 + MPP X0 + TICK + CX 0 1 + TICK + H 0 + MX 1 + TICK + MPP Z0 + DETECTOR rec[-1] rec[-2] rec[-3] + """ + ) + ) + chunk.verify() + assert chunk == stimflow.Chunk( + q2i={1 + 2j: 0, 1 + 3j: 1}, + flows=[ + stimflow.Flow( + start=stimflow.PauliMap.from_xs([1 + 2j]), + end=stimflow.PauliMap.from_zs([1 + 2j]), + mids=(0,), + obs_key=None, + center=1 + 2j, + ) + ], + circuit=stim.Circuit( + """ + CX 0 1 + TICK + H 0 + MX 1 + """ + ), + ) + + chunk = stimflow.Chunk.from_circuit_with_mpp_boundaries( + stim.Circuit( + """ + QUBIT_COORDS(1, 2) 0 + QUBIT_COORDS(1, 3) 1 + MPP X0 + OBSERVABLE_INCLUDE(0) rec[-1] + TICK + CX 0 1 + TICK + MX 1 + OBSERVABLE_INCLUDE(0) rec[-1] + H 0 + TICK + MPP Z0 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ) + ) + chunk.verify() + assert chunk == stimflow.Chunk( + q2i={1 + 2j: 0, 1 + 3j: 1}, + flows=[ + stimflow.Flow( + start=stimflow.PauliMap.from_xs([1 + 2j]), + end=stimflow.PauliMap.from_zs([1 + 2j]), + mids=(0,), + obs_key=0, + center=1 + 2j, + ) + ], + circuit=stim.Circuit( + """ + CX 0 1 + TICK + MX 1 + H 0 + """ + ), + ) + + +def test_from_circuit_with_mpp_boundaries_complex(): + chunk = stimflow.Chunk.from_circuit_with_mpp_boundaries( + stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1, 1) 3 + QUBIT_COORDS(1, 2) 4 + QUBIT_COORDS(2, 0) 5 + QUBIT_COORDS(2, 1) 6 + QUBIT_COORDS(2, 2) 7 + QUBIT_COORDS(2, 3) 8 + QUBIT_COORDS(3, 0) 9 + QUBIT_COORDS(3, 1) 10 + QUBIT_COORDS(3, 2) 11 + QUBIT_COORDS(3, 3) 12 + QUBIT_COORDS(4, 0) 13 + QUBIT_COORDS(4, 1) 14 + QUBIT_COORDS(4, 2) 15 + QUBIT_COORDS(4, 3) 16 + QUBIT_COORDS(5, 3) 17 + #!pragma POLYGON(0,0,1,0.25) 11 15 9 6 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 8 17 15 11 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + TICK + R 0 9 15 11 6 3 17 8 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + DETECTOR(2, 2, 0) rec[-8] + DETECTOR(2, 0, 0) rec[-7] + DETECTOR(4, 3, 0) rec[-6] + DETECTOR(4, 1, 0) rec[-5] + TICK + R 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + MY 17 + DETECTOR(2, 2, 1) rec[-9] + DETECTOR(2, 0, 1) rec[-8] + DETECTOR(4, 3, 1) rec[-7] + DETECTOR(4, 1, 1) rec[-6] + DETECTOR(1, 2, 1) rec[-5] rec[-13] + DETECTOR(1, 0, 1) rec[-4] rec[-12] + DETECTOR(3, 1, 1) rec[-3] rec[-11] + DETECTOR(3, 3, 1) rec[-2] rec[-10] + TICK + #!pragma POLYGON(0,0,1,0.25) 11 15 9 6 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + #!pragma POLYGON(1,1,0,0.25) 9 13 11 6 + TICK + R 13 + RX 14 10 5 2 7 1 + S 15 11 8 3 9 6 0 + TICK + CX 14 15 1 0 10 9 7 8 5 6 2 3 + TICK + CX 3 1 6 7 10 14 + TICK + CX 6 3 10 11 15 14 + TICK + CX 6 10 14 13 + TICK + MX 6 + DETECTOR(3, 3, 2) rec[-1] rec[-2] rec[-7] rec[-8] rec[-10] rec[-11] rec[-13] rec[-15] rec[-16] rec[-18] + TICK + #!pragma POLYGON(0,0,1,0.25) 9 13 11 6 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + #!pragma POLYGON(1,1,0,0.25) 11 15 9 6 + TICK + RX 6 + TICK + CX 6 10 15 14 + TICK + CX 6 3 10 11 14 13 + TICK + CX 3 1 6 7 10 14 + TICK + CX 1 0 10 9 7 8 14 13 5 6 2 3 + TICK + MX 14 10 5 2 7 1 15 + S 6 11 13 9 8 3 0 + DETECTOR(3, 1, 3) rec[-6] + DETECTOR(1, 0, 3) rec[-4] + DETECTOR(2, 2, 3) rec[-3] + DETECTOR(0, 1, 3) rec[-2] + DETECTOR(2, 1, 3) rec[-1] rec[-2] rec[-3] rec[-4] rec[-5] rec[-6] rec[-7] rec[-8] + DETECTOR(4, 2, 3) rec[-1] rec[-7] + TICK + #!pragma POLYGON(0,0,1,0.25) 13 9 6 11 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + TICK + MPP X8*X11*X6*X3 + DETECTOR(1, 2, 4) rec[-1] rec[-6] rec[-14] + TICK + MPP X13*X9*X6*X11 + DETECTOR(3, 1, 5) rec[-1] rec[-3] rec[-7] rec[-13] + TICK + MPP X9*X0*X3*X6 + DETECTOR(1, 0, 6) rec[-1] rec[-8] rec[-15] + TICK + MPP Z8*Z11*Z6*Z3 + DETECTOR(2, 0, 7) rec[-1] rec[-20] rec[-21] rec[-28] rec[-29] + TICK + MPP Z9*Z0*Z3*Z6 + DETECTOR(2, 2, 8) rec[-1] rec[-22] rec[-30] + TICK + MPP Z13*Z9*Z6*Z11 + DETECTOR(4, 1, 9) rec[-1] rec[-20] rec[-21] rec[-28] rec[-29] + TICK + MPP Y13*Y9*Y0*Y6*Y3*Y8*Y11 + OBSERVABLE_INCLUDE(0) rec[-1] rec[-9] rec[-10] rec[-13] rec[-14] + """ # noqa: E501 + ) + ) + chunk.verify() + assert len(chunk.flows) == 7 + assert all(not e.start for e in chunk.flows) + assert chunk.circuit.num_detectors == 25 - 6 + assert chunk.circuit == stim.Circuit( + """ + R 0 9 15 11 6 3 17 8 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + DETECTOR(2, 2, 0) rec[-8] + DETECTOR(2, 0, 0) rec[-7] + DETECTOR(4, 3, 0) rec[-6] + DETECTOR(4, 1, 0) rec[-5] + TICK + R 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + MY 17 + DETECTOR(2, 2, 1) rec[-9] + DETECTOR(2, 0, 1) rec[-8] + DETECTOR(4, 3, 1) rec[-7] + DETECTOR(4, 1, 1) rec[-6] + DETECTOR(1, 2, 1) rec[-5] rec[-13] + DETECTOR(1, 0, 1) rec[-4] rec[-12] + DETECTOR(3, 1, 1) rec[-3] rec[-11] + DETECTOR(3, 3, 1) rec[-2] rec[-10] + TICK + TICK + R 13 + RX 14 10 5 2 7 1 + S 15 11 8 3 9 6 0 + TICK + CX 14 15 1 0 10 9 7 8 5 6 2 3 + TICK + CX 3 1 6 7 10 14 + TICK + CX 6 3 10 11 15 14 + TICK + CX 6 10 14 13 + TICK + MX 6 + DETECTOR(3, 3, 2) rec[-1] rec[-2] rec[-7] rec[-8] rec[-10] rec[-11] rec[-13] rec[-15] rec[-16] rec[-18] + TICK + TICK + RX 6 + TICK + CX 6 10 15 14 + TICK + CX 6 3 10 11 14 13 + TICK + CX 3 1 6 7 10 14 + TICK + CX 1 0 10 9 7 8 14 13 5 6 2 3 + TICK + MX 14 10 5 2 7 1 15 + S 6 11 13 9 8 3 0 + DETECTOR(3, 1, 3) rec[-6] + DETECTOR(1, 0, 3) rec[-4] + DETECTOR(2, 2, 3) rec[-3] + DETECTOR(0, 1, 3) rec[-2] + DETECTOR(2, 1, 3) rec[-1] rec[-2] rec[-3] rec[-4] rec[-5] rec[-6] rec[-7] rec[-8] + DETECTOR(4, 2, 3) rec[-1] rec[-7] + """ # noqa: E501 + ) + + +def test_chunk_viewer(): + chunk = stimflow.Chunk( + circuit=stim.Circuit( + """ + R 0 1 2 3 4 + CX 2 0 + M 0 + """ + ), + q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, + flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({}), mids=[0], end=stimflow.PauliMap({1: "Z"}))], + ) + assert chunk.to_html_viewer() is not None + + +def test_anticommuting_obs_flows(): + chunk = stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + QUBIT_COORDS(0, 1) 2 + QUBIT_COORDS(1, 1) 3 + DEPOLARIZE1(0.001) 0 1 2 3 + MPP X0*X1*X2*X3 + MZZ 0 1 2 3 + """ + ), + flows=[ + stimflow.Flow(start=stimflow.PauliMap({"X": [0, 1, 1j, 1 + 1j]}), mids=[0]), + stimflow.Flow(end=stimflow.PauliMap({"X": [0, 1, 1j, 1 + 1j]}), mids=[0]), + stimflow.Flow(start=stimflow.PauliMap({"Z": [0, 1]}), mids=[1]), + stimflow.Flow(end=stimflow.PauliMap({"Z": [0, 1]}), mids=[1]), + stimflow.Flow(start=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), mids=[2]), + stimflow.Flow(end=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), mids=[2]), + stimflow.Flow( + start=stimflow.PauliMap({"X": [0, 1]}), end=stimflow.PauliMap({"X": [0, 1]}), obs_key="X" + ), + stimflow.Flow( + start=stimflow.PauliMap({"Z": [0, 1j]}), end=stimflow.PauliMap({"Z": [0, 1j]}), obs_key="Z" + ), + ], + ) + chunk.verify() + assert chunk.to_closed_circuit() == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1, 1) 3 + OBSERVABLE_INCLUDE(0) X0 X2 + TICK + OBSERVABLE_INCLUDE(1) Z0 Z1 + TICK + MPP X0*X1*X2*X3 + TICK + MPP Z0*Z2 Z1*Z3 + TICK + DEPOLARIZE1(0.001) 0 2 1 3 + MPP X0*X2*X1*X3 + MZZ 0 2 1 3 + DETECTOR(0.5, 0.5, 0) rec[-6] rec[-3] + DETECTOR(0.5, 0, 0) rec[-5] rec[-2] + DETECTOR(0.5, 1, 0) rec[-4] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + MPP X0*X1*X2*X3 + TICK + MPP Z0*Z2 Z1*Z3 + DETECTOR(0.5, 0.5, 0) rec[-6] rec[-3] + DETECTOR(0.5, 0, 0) rec[-5] rec[-2] + DETECTOR(0.5, 1, 0) rec[-4] rec[-1] + TICK + OBSERVABLE_INCLUDE(0) X0 X2 + TICK + OBSERVABLE_INCLUDE(1) Z0 Z1 + """ + ) + assert chunk.find_distance(max_search_weight=2, skip_adding_noise=True) == 2 + assert chunk.find_distance(max_search_weight=3, skip_adding_noise=True) == 2 + + +def test_embedded_observables(): + chunk = stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + M 0 + OBSERVABLE_INCLUDE(2) rec[-1] + """ + ), + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_name("L2"))], + o2i={"L2": 2}, + ) + chunk.verify() diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util.py b/glue/stimflow/src/stimflow/_chunk/_code_util.py new file mode 100644 index 00000000..d6fcf638 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_code_util.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +import collections +from typing import cast, TYPE_CHECKING + +import stim + +from stimflow._chunk._stabilizer_code import StabilizerCode +from stimflow._core import PauliMap + +if TYPE_CHECKING: + from stimflow._chunk._chunk_builder import ChunkBuilder + from stimflow._chunk._chunk import Chunk + from stimflow._chunk._chunk_reflow import ChunkReflow + + +def circuit_to_cycle_code_slices(circuit: stim.Circuit) -> dict[int, StabilizerCode]: + from stimflow._chunk._patch import Patch + from stimflow._chunk._stabilizer_code import StabilizerCode + + t = 0 + ticks = set() + for inst in circuit.flattened(): + if inst.name == "TICK": + t += 1 + elif inst.name in ["R", "RX"]: + if t - 1 not in ticks and t - 2 not in ticks: + ticks.add(max(t - 1, 0)) + elif inst.name in ["M", "MX"]: + ticks.add(t) + + regions = circuit.detecting_regions(ticks=ticks) + layers: dict[int, list[tuple[stim.DemTarget, stim.PauliString]]] = collections.defaultdict(list) + for dem_target, tick2paulis in regions.items(): + for tick, pauli_string in tick2paulis.items(): + layers[tick].append((dem_target, pauli_string)) + + i2q = {k: r + i * 1j for k, (r, i) in circuit.get_final_qubit_coordinates().items()} + + codes = {} + for tick, layer in sorted(layers.items()): + obs = [] + tiles = [] + for dem_target, pauli_string in layer: + pauli_map = PauliMap( + {i2q[q]: "_XYZ"[pauli_string[q]] for q in pauli_string.pauli_indices()} + ) + if dem_target.is_relative_detector_id(): + tiles.append(pauli_map.to_tile().with_edits(flags={str(dem_target.val)})) + else: + obs.append(pauli_map) + codes[tick] = StabilizerCode(stabilizers=Patch(tiles), logicals=obs) + + return codes + + +def find_d1_error( + obj: stim.Circuit | stim.DetectorErrorModel, +) -> stim.ExplainedError | stim.DemInstruction | None: + circuit: stim.Circuit | None + dem: stim.DetectorErrorModel + if isinstance(obj, stim.Circuit): + circuit = obj + dem = circuit.detector_error_model() + elif isinstance(obj, stim.DetectorErrorModel): + circuit = None + dem = obj + else: + raise NotImplementedError(f"{obj=}") + + for inst in dem: + if inst.type == "error": + dets: set[int] = set() + obs: set[int] = set() + for target in inst.targets_copy(): + if target.is_relative_detector_id(): + dets ^= {target.val} + elif target.is_logical_observable_id(): + obs ^= {target.val} + if obs and not dets: + if circuit is None: + return inst + filter_det = stim.DetectorErrorModel() + filter_det.append(inst) + return circuit.explain_detector_error_model_errors( + dem_filter=filter_det, reduce_to_one_representative_error=True + )[0] + + return None + + +def find_d2_error( + obj: stim.Circuit | stim.DetectorErrorModel, +) -> list[stim.ExplainedError] | stim.DetectorErrorModel | None: + d1 = find_d1_error(obj) + if d1 is not None: + if isinstance(d1, stim.DemInstruction): + result = stim.DetectorErrorModel() + result.append(d1) + return result + return [d1] + + if isinstance(obj, stim.Circuit): + circuit = obj + dem = circuit.detector_error_model() + elif isinstance(obj, stim.DetectorErrorModel): + circuit = None + dem = obj + else: + raise NotImplementedError(f"{obj=}") + + seen = {} + for inst in dem.flattened(): + if inst.type == "error": + dets_mut: set[int] = set() + obs_mut: set[int] = set() + for target in inst.targets_copy(): + if target.is_relative_detector_id(): + dets_mut ^= {target.val} + elif target.is_logical_observable_id(): + obs_mut ^= {target.val} + dets = frozenset(dets_mut) + obs = frozenset(obs_mut) + if dets not in seen: + seen[dets] = (obs, inst) + elif seen[dets][0] != obs: + filter_det = stim.DetectorErrorModel() + filter_det.append(inst) + filter_det.append(seen[dets][1]) + if circuit is None: + return filter_det + return circuit.explain_detector_error_model_errors( + dem_filter=filter_det, reduce_to_one_representative_error=True + ) + return None + + +def verify_distance_is_at_least_2(obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode): + __tracebackhide__ = True + if isinstance(obj, StabilizerCode): + obj.verify_distance_is_at_least_2() + return + err = find_d1_error(obj) + if err is not None: + raise ValueError(f"Found a distance 1 error: {err}") + + +def verify_distance_is_at_least_3(obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode): + __tracebackhide__ = True + err = find_d2_error(obj) + if err is not None: + raise ValueError(f"Found a distance {len(err)} error: {err}") + + +def transversal_code_transition_chunks( + *, prev_code: StabilizerCode, next_code: StabilizerCode, measured: PauliMap, reset: PauliMap +) -> tuple[Chunk, ChunkReflow, Chunk]: + from stimflow._chunk._chunk_reflow import ChunkReflow + + def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: + for q, p in original.items(): + if dissipated.get(q, p) != p: + # Anticommutes. + return None + return PauliMap( + {q: p for q, p in original.items() if q not in dissipated}, name=original.name + ) + + from stimflow._chunk._chunk_builder import ChunkBuilder + prev_builder = ChunkBuilder(prev_code.data_set) + prev_key2obs = {} + for b in "XYZ": + prev_builder.append( + f"M{b}", prev_code.data_set & {q for q, p in measured.items() if p == b} + ) + start: PauliMap | None + end: PauliMap | None + for tile in prev_code.tiles: + start = tile.to_pauli_map() + end = clipped(start, measured) + if end is None: + prev_builder.add_discarded_flow_input(tile) + else: + prev_builder.add_flow(start=tile, end=end, ms=start.keys() - end.keys()) + for k, obs in enumerate(prev_code.flat_logicals): + assert obs.name is not None + end = clipped(obs, measured) + if end is None: + prev_builder.add_discarded_flow_input(obs) + else: + prev_key2obs[obs.name] = end + prev_builder.add_flow(start=obs, end=end, ms=obs.keys() - end.keys()) + + next_builder = ChunkBuilder(next_code.data_set) + next_obs2key = {} + for b in "XYZ": + next_builder.append(f"R{b}", next_code.data_set & {q for q, p in reset.items() if p == b}) + for tile in next_code.tiles: + end = tile.to_pauli_map() + start = clipped(end, reset) + if start is None: + next_builder.add_discarded_flow_output(tile) + else: + next_builder.add_flow(start=start, end=tile) + for obs in next_code.flat_logicals: + assert obs.name is not None + start = clipped(obs, reset) + if start is None: + next_builder.add_discarded_flow_output(obs) + else: + next_obs2key[obs.name] = start + next_builder.add_flow(start=start, end=obs) + + prev_chunk = prev_builder.finish_chunk(wants_to_merge_with_prev=True) + reflow = ChunkReflow.from_auto_rewrite_transitions_using_stable( + stable=[ + cast(PauliMap, flow.start) + for flow in next_builder.flows + if flow.obs_key is None + if flow.start + ], + transitions=[ + (prev_key2obs[obs_key], next_obs2key[obs_key]) + for obs_key in next_obs2key.keys() & prev_key2obs.keys() + ], + ) + next_chunk = next_builder.finish_chunk(wants_to_merge_with_next=True) + return prev_chunk, reflow, next_chunk diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util_test.py b/glue/stimflow/src/stimflow/_chunk/_code_util_test.py new file mode 100644 index 00000000..79a84f63 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_code_util_test.py @@ -0,0 +1,165 @@ +import pytest +import stim + +import stimflow + + +def test_verify_distance_is_at_least_23(): + stimflow.verify_distance_is_at_least_2( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + """ + ) + ) + + stimflow.verify_distance_is_at_least_2( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + DETECTOR rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ) + ) + + stimflow.verify_distance_is_at_least_2( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + DETECTOR rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ).detector_error_model() + ) + + with pytest.raises(ValueError, match="distance 1 error"): + stimflow.verify_distance_is_at_least_2( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ) + ) + + with pytest.raises(ValueError, match="distance 1 error"): + stimflow.verify_distance_is_at_least_3( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ) + ) + + stimflow.verify_distance_is_at_least_2( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=2, + rounds=3, + after_clifford_depolarization=1e-3, + ) + ) + + stimflow.verify_distance_is_at_least_2( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=3, + rounds=3, + after_clifford_depolarization=1e-3, + ) + ) + + stimflow.verify_distance_is_at_least_2( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=9, + rounds=3, + after_clifford_depolarization=1e-3, + ) + ) + + stimflow.verify_distance_is_at_least_3( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=3, + rounds=3, + after_clifford_depolarization=1e-3, + ) + ) + + stimflow.verify_distance_is_at_least_3( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=9, + rounds=3, + after_clifford_depolarization=1e-3, + ) + ) + + with pytest.raises(ValueError, match="distance 2 error"): + stimflow.verify_distance_is_at_least_3( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=2, + rounds=3, + after_clifford_depolarization=1e-3, + ) + ) + + +def test_transversal_code_transition_chunk(): + prev_code = stimflow.StabilizerCode( + stabilizers=[ + stimflow.PauliMap.from_zs([0, 1]), + stimflow.PauliMap.from_zs([1 + 2j, 2 + 2j]), + stimflow.PauliMap.from_xs([1j, 2j]), + stimflow.PauliMap.from_xs([2, 2 + 1j]), + stimflow.PauliMap.from_xs([0 + 0j, 1 + 0j, 0 + 1j, 1 + 1j]), + stimflow.PauliMap.from_zs([1 + 0j, 2 + 0j, 1 + 1j, 2 + 1j]), + stimflow.PauliMap.from_zs([0 + 1j, 1 + 1j, 0 + 2j, 1 + 2j]), + stimflow.PauliMap.from_xs([1 + 1j, 2 + 1j, 1 + 2j, 2 + 2j]), + ], + logicals=[ + ( + stimflow.PauliMap.from_xs([0, 1, 2]).with_name("X"), + stimflow.PauliMap.from_zs([0j + 1, 1j + 1, 2j + 1]).with_name("Z"), + ) + ], + ) + prev_code.verify() + assert prev_code.find_distance(max_search_weight=2) == 3 + next_code = prev_code.with_transformed_coords(lambda e: -e.real + e.imag * 1j + 3) + + prev_chunk, reflow, next_chunk = stimflow.transversal_code_transition_chunks( + prev_code=prev_code, + next_code=next_code, + measured=stimflow.PauliMap.from_xs(prev_code.data_set - next_code.data_set), + reset=stimflow.PauliMap.from_xs(next_code.data_set - prev_code.data_set), + ) + prev_chunk.verify(expected_in=prev_code) + next_chunk.verify(expected_out=next_code) + reflow.verify(expected_in=prev_chunk.end_interface(), expected_out=next_chunk.start_interface()) + + compiler = stimflow.ChunkCompiler() + compiler.append_magic_init_chunk() + compiler.append(prev_chunk) + compiler.append(reflow) + compiler.append(next_chunk) + compiler.append_magic_end_chunk() + circuit = compiler.finish_circuit() + noisy_circuit = stimflow.NoiseModel.uniform_depolarizing(1e-3).noisy_circuit_skipping_mpp_boundaries( + circuit + ) + assert len(noisy_circuit.shortest_graphlike_error()) == 2 diff --git a/glue/stimflow/src/stimflow/_chunk/_flow_metadata.py b/glue/stimflow/src/stimflow/_chunk/_flow_metadata.py new file mode 100644 index 00000000..a3747636 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_flow_metadata.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from collections.abc import Iterable + + +class FlowMetadata: + """Metadata, based on a flow, to use during circuit generation.""" + + def __init__(self, *, extra_coords: Iterable[float] = (), tag: str | None = ""): + """ + + Args: + extra_coords: Extra numbers to add to DETECTOR coordinate arguments. By default stimflow + gives each detector an X, Y, and T coordinate. These numbers go afterward. + tag: A tag to attach to DETECTOR or OBSERVABLE_INCLUDE instructions. + """ + self.extra_coords: tuple[float, ...] = tuple(extra_coords) + self.tag: str = tag or "" + + def __eq__(self, other) -> bool: + if isinstance(other, FlowMetadata): + return self.extra_coords == other.extra_coords and self.tag == other.tag + return NotImplemented + + def __hash__(self) -> int: + return hash((FlowMetadata, self.extra_coords, self.tag)) + + def __repr__(self): + return f"stimflow.FlowMetadata(extra_coords={self.extra_coords!r}, tag={self.tag!r})" diff --git a/glue/stimflow/src/stimflow/_chunk/_flow_util.py b/glue/stimflow/src/stimflow/_chunk/_flow_util.py new file mode 100644 index 00000000..e477ea47 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_flow_util.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable, Iterable, Mapping, Set +from typing import Any, cast + +import numpy as np +import stim + +from stimflow._core import Flow, PauliMap, xor_sorted + + +def _solve_auto_flow_starts( + *, + flows: Iterable[Flow], + circuit: stim.Circuit, + q2i: dict[complex, int], + failure_out: list[Flow], +) -> list[Flow]: + + num_qubits = max(circuit.num_qubits, max([i + 1 for i in q2i.values()], default=0)) + i2q = {i: q for q, i in q2i.items()} + + new_flows = [] + for flow in flows: + stim_end = cast(PauliMap, flow.end).to_stim_pauli_string(q2i, num_qubits=num_qubits) + try: + stim_start = stim_end.before(circuit) + except ValueError: + failure_out.append(flow) + continue + start = PauliMap({i2q[q]: "_XYZ"[stim_start[q]] for q in stim_start.pauli_indices()}) + new_flows.append(flow.with_edits(start=start)) + + return new_flows + + +def _solve_auto_flow_ends( + *, + flows: Iterable[Flow], + circuit: stim.Circuit, + q2i: dict[complex, int], + failure_out: list[Flow], +) -> list[Flow]: + + num_qubits = circuit.num_qubits + i2q = {i: q for q, i in q2i.items()} + + new_flows = [] + for flow in flows: + stim_start = cast(PauliMap, flow.start).to_stim_pauli_string(q2i, num_qubits=num_qubits) + try: + stim_end = stim_start.after(circuit) + except ValueError: + failure_out.append(flow) + continue + end = PauliMap({i2q[q]: "_XYZ"[stim_end[q]] for q in stim_end.pauli_indices()}) + new_flows.append(flow.with_edits(end=end)) + + return new_flows + + +def _has_obs_include_instructions(circuit: stim.Circuit) -> bool: + for inst in circuit: + if inst.name == "OBSERVABLE_INCLUDE": + return True + elif inst.name == "REPEAT": + return _has_obs_include_instructions(inst.body_copy()) + return False + + +def _solve_auto_flow_ms( + *, + flows: Iterable[Flow], + circuit: stim.Circuit, + q2i: dict[complex, int], + o2i: dict[Any, int], + failure_out: list[Flow], +) -> list[Flow]: + result: list[Flow] = list(flows) + + stim_flows: list[stim.Flow] = [] + has_obs_with_auto_measurements = False + sub_o2i: Mapping[Any, int | None] = dict(o2i) + if not _has_obs_include_instructions(circuit): + sub_o2i = collections.defaultdict(lambda: None) + for k, flow in enumerate(result): + flow = result[k] + has_obs_with_auto_measurements |= flow.obs_key is not None + stim_flows.append(flow.to_stim_flow(q2i=q2i, o2i=sub_o2i)) + + if has_obs_with_auto_measurements and circuit.num_observables: + raise NotImplementedError( + "The circuit contains OBSERVABLE_INCLUDE instructions, " + "and a flow with auto-solved measurements mentions an observable." + ) + + if stim_flows: + measurements = circuit.solve_flow_measurements(stim_flows) + for k in range(len(measurements)): + if measurements[k] is None: + failure_out.append(result[k]) + result[k] = result[k].with_edits(measurement_indices=[]) + else: + result[k] = result[k].with_edits(measurement_indices=measurements[k]) + + return result + + +def mbqc_to_unitary_by_solving_feedback( + mbqc_circuit: stim.Circuit, + *, + desired_flow_generators: list[stim.Flow] | None = None, + num_relevant_qubits: int, +) -> stim.Circuit: + """Converts an MBQC circuit to a unitary circuit by adding Pauli feedback. + + Args: + mbqc_circuit: The circuit to add feedback to. + desired_flow_generators: Defaults to None (clear all measurement + dependence and negative signs). When set to a list, it specifies + reference signs and measurement dependencies to keep. + num_relevant_qubits: The number of non-ancillary qubits. + + Returns: + The circuit with added Pauli feedback. + """ + num_qubits = mbqc_circuit.num_qubits + num_measurements = mbqc_circuit.num_measurements + num_added_dof = num_relevant_qubits * 2 + + # Add feedback from extra qubits, so `flow_generators` includes X/Z feedback terms. + augmented_circuit = mbqc_circuit.copy() + for q in range(num_relevant_qubits): + augmented_circuit.append("M", [num_qubits + q * 2]) + augmented_circuit.append("CX", [stim.target_rec(-1), q]) + augmented_circuit.append("M", [num_qubits + q * 2 + 1]) + augmented_circuit.append("CZ", [stim.target_rec(-1), q]) + + # Diagonalize the flow generators. + # - Remove terms mentioning ancillary qubits. + flow_table: list[tuple[stim.PauliString, stim.PauliString, list[int]]] = [ + (f.input_copy(), f.output_copy(), f.measurements_copy()) + for f in augmented_circuit.flow_generators() + ] + num_solved_flows = 0 + pivot_funcs = [ + lambda f, i: len(f[0]) > i and 1 <= f[0][i] <= 2, + lambda f, i: len(f[0]) > i and 2 <= f[0][i] <= 3, + lambda f, i: len(f[1]) > i and 1 <= f[1][i] <= 2, + lambda f, i: len(f[1]) > i and 2 <= f[1][i] <= 3, + ] + + def elim_step(q: int, func: Callable): + nonlocal num_solved_flows + for pivot in range(num_solved_flows, len(flow_table)): + if func(flow_table[pivot], q): + break + else: + return + for row in range(len(flow_table)): + if pivot != row and func(flow_table[row], q): + i1, o1, m1 = flow_table[row] + i2, o2, m2 = flow_table[pivot] + flow_table[row] = (i1 * i2, o1 * o2, xor_sorted(m1 + m2)) + if pivot != num_solved_flows: + flow_table[num_solved_flows], flow_table[pivot] = ( + flow_table[pivot], + flow_table[num_solved_flows], + ) + num_solved_flows += 1 + + for q in range(num_relevant_qubits, augmented_circuit.num_qubits): + for func in pivot_funcs: + elim_step(q, func) + flow_table = flow_table[num_solved_flows:] + num_solved_flows = 0 + for q in range(num_relevant_qubits): + for func in pivot_funcs[:2]: + elim_step(q, func) + for q in range(num_relevant_qubits): + for func in pivot_funcs[2:]: + elim_step(q, func) + + if desired_flow_generators is not None: + # TODO: make this work even if the desired generators are in a different basis. + for k in range(len(desired_flow_generators)): + i1 = desired_flow_generators[k].input_copy() + i2 = flow_table[k][0] + assert (i1 * i2).weight == 0 + o1 = desired_flow_generators[k].output_copy() + o2 = flow_table[k][1] + assert (o1 * o2).weight == 0 + flow_table[k] = ( + i1 * i2.sign, + o1 * o2.sign, + xor_sorted(flow_table[k][2] + desired_flow_generators[k].measurements_copy()), + ) + + # Construct a feedback table describing how each measurement affects each flow. + feedback_table: list[np.ndarray] = [] + for g in flow_table: + i2 = g[0].pauli_indices() + o2 = g[1].pauli_indices() + if i2 and i2[0] >= num_relevant_qubits: + continue + if o2 and o2[0] >= num_relevant_qubits: + continue + row2 = np.zeros(num_measurements + num_added_dof + 1, dtype=np.bool_) + for k in g[2]: + row2[k] ^= 1 + row2[-1] ^= g[0].sign * g[1].sign == -1 + feedback_table.append(row2) + + # Diagonalize the feedback table. + num_solved = 0 + for k in range(num_measurements, num_measurements + num_added_dof): + for pivot in range(num_solved, len(feedback_table)): + if feedback_table[pivot][k]: + break + else: + continue + for row in range(len(feedback_table)): + if pivot != row and feedback_table[row][k]: + feedback_table[row] ^= feedback_table[pivot] + if pivot != num_solved: + feedback_table[num_solved] ^= feedback_table[pivot] + feedback_table[pivot] ^= feedback_table[num_solved] + feedback_table[num_solved] ^= feedback_table[pivot] + num_solved += 1 + + result = mbqc_circuit.copy() + + # Convert from table to dicts. + cx: collections.defaultdict[int | None, set[int]] = collections.defaultdict(set) + cz: collections.defaultdict[int | None, set[int]] = collections.defaultdict(set) + for q in range(num_added_dof): + assert np.array_equal(np.flatnonzero(feedback_table[q][-num_added_dof - 1 : -1]), [q]) + for q in range(num_relevant_qubits): + if feedback_table[2 * q + 0][-1]: + cx[None].add(q) + for m in np.flatnonzero(feedback_table[2 * q + 0][: -num_added_dof - 1]): + cx[m - num_measurements].add(q) + for q in range(num_relevant_qubits): + if feedback_table[2 * q + 1][-1]: + cz[None].add(q) + for m in np.flatnonzero(feedback_table[2 * q + 1][:-num_added_dof]): + cz[m - num_measurements].add(q) + + # Output deterministic Paulis. + for q in sorted(cx[None] - cz[None]): + result.append("X", [q]) + for q in sorted(cx[None] & cz[None]): + result.append("Y", [q]) + for q in sorted(cz[None] - cx[None]): + result.append("Z", [q]) + cx.pop(None, None) + cz.pop(None, None) + cx_keys = cast(Set[int], cx.keys()) + cz_keys = cast(Set[int], cz.keys()) + + # Output feedback Paulis. + for k in cx_keys: + for q in sorted(cx[k] - cz[k]): + result.append("CX", [stim.target_rec(k), q]) + for k in cx_keys: + for q in sorted(cx[k] & cz[k]): + result.append("CY", [stim.target_rec(k), q]) + for k in cz_keys: + for q in sorted(cz[k] - cx[k]): + result.append("CZ", [stim.target_rec(k), q]) + + return result diff --git a/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py b/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py new file mode 100644 index 00000000..dc93a97d --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py @@ -0,0 +1,164 @@ +import stim + +import stimflow +from stimflow._chunk._flow_util import ( + _solve_auto_flow_ms, + mbqc_to_unitary_by_solving_feedback, +) + + +def test_solve_flow_auto_measurements(): + failure_out = [] + assert ( + _solve_auto_flow_ms( + flows=[ + stimflow.Flow( + start=stimflow.PauliMap({"Z": [0 + 1j, 2 + 1j]}), center=-1, flags={"X"} + ) + ], + circuit=stim.Circuit( + """ + R 1 + CX 0 1 2 1 + M 1 + """ + ), + q2i={0 + 1j: 0, 1 + 1j: 1, 2 + 1j: 2}, + o2i={}, + failure_out=failure_out, + ) + == [ + stimflow.Flow( + start=stimflow.PauliMap({"Z": [0 + 1j, 2 + 1j]}), mids=[0], center=-1, flags={"X"} + ), + ] + ) + assert not failure_out + + +def test_solve_flow_auto_flow_measurements_with_observable(): + failure_out = [] + assert ( + _solve_auto_flow_ms( + flows=[ + stimflow.Flow( + start=stimflow.PauliMap.from_xs([1, 2]), + end=stimflow.PauliMap.from_zs([1, 2]), + obs_key="L2", + ) + ], + circuit=stim.Circuit( + """ + MYY 1 2 + """ + ), + q2i={1: 1, 2: 2}, + o2i={"L2": 3}, + failure_out=[], + ) + == [ + stimflow.Flow( + start=stimflow.PauliMap.from_xs([1, 2]), + end=stimflow.PauliMap.from_zs([1, 2]), + mids=[0], + obs_key="L2", + ), + ] + ) + assert not failure_out + + +def test_mbqc_to_unitary_by_solving_feedback(): + assert ( + mbqc_to_unitary_by_solving_feedback( + stim.Circuit( + """ + MX 1 + MZZ 0 1 + MY 1 + MXX 0 1 + MZ 1 + MX 1 + MZZ 0 1 + MY 1 + """ + ), + desired_flow_generators=stim.gate_data("H").flows, + num_relevant_qubits=1, + ) + == stim.Circuit( + """ + MX 1 + MZZ 0 1 + MY 1 + MXX 0 1 + M 1 + MX 1 + MZZ 0 1 + MY 1 + CX rec[-8] 0 rec[-7] 0 + CY rec[-5] 0 rec[-4] 0 + CZ rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 + """ + ) + ) + + assert ( + mbqc_to_unitary_by_solving_feedback( + stim.Circuit( + """ + MX 1 + MZZ 0 1 + MY 1 + MXX 0 1 + MZ 1 + MX 1 + MZZ 0 1 + MY 1 + """ + ), + desired_flow_generators=stim.gate_data("SQRT_Y").flows, + num_relevant_qubits=1, + ) + == stim.Circuit( + """ + MX 1 + MZZ 0 1 + MY 1 + MXX 0 1 + M 1 + MX 1 + MZZ 0 1 + MY 1 + X 0 + CX rec[-8] 0 rec[-7] 0 + CY rec[-5] 0 rec[-4] 0 + CZ rec[-6] 0 rec[-3] 0 rec[-2] 0 rec[-1] 0 + """ + ) + ) + + assert ( + mbqc_to_unitary_by_solving_feedback( + stim.Circuit( + """ + MX 2 + MZZ 0 2 + MXX 1 2 + MZ 2 + """ + ), + desired_flow_generators=stim.gate_data("CX").flows, + num_relevant_qubits=2, + ) + == stim.Circuit( + """ + MX 2 + MZZ 0 2 + MXX 1 2 + M 2 + CX rec[-3] 1 rec[-1] 1 + CZ rec[-4] 0 rec[-2] 0 + """ + ) + ) diff --git a/glue/stimflow/src/stimflow/_chunk/_patch.py b/glue/stimflow/src/stimflow/_chunk/_patch.py new file mode 100644 index 00000000..dfeef76b --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_patch.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +import collections +import functools +from collections.abc import Callable, Iterable, Iterator +from typing import Literal, overload, TYPE_CHECKING + +from stimflow._core import PauliMap, str_svg, Tile + +if TYPE_CHECKING: + from stimflow._chunk._stabilizer_code import StabilizerCode + + +class Patch: + """A collection of annotated stabilizers.""" + + def __init__(self, tiles: Iterable[Tile | PauliMap], *, do_not_sort: bool = False): + kept_tiles = [] + for tile in tiles: + if isinstance(tile, Tile): + kept_tiles.append(tile) + elif isinstance(tile, PauliMap): + kept_tiles.append(tile.to_tile()) + else: + raise ValueError(f"Don't know how to interpret this as a stimflow.Tile: {tile=}") + if not do_not_sort: + kept_tiles = sorted(kept_tiles) + + self.tiles: tuple[Tile, ...] = tuple(kept_tiles) + + def __len__(self) -> int: + return len(self.tiles) + + @overload + def __getitem__(self, item: int) -> Tile: + pass + + @overload + def __getitem__(self, item: slice) -> Patch: + pass + + def __getitem__(self, item: int | slice) -> Patch | Tile: + if isinstance(item, slice): + return Patch(self.tiles[item]) + if isinstance(item, int): + return self.tiles[item] + raise NotImplementedError(f"{item=}") + + def __iter__(self) -> Iterator[Tile]: + return self.tiles.__iter__() + + @functools.cached_property + def partitioned_tiles(self) -> tuple[tuple[Tile, ...], ...]: + """Returns the tiles of the patch, but split into non-overlapping groups.""" + qubit_used: set[tuple[complex, int]] = set() + layers: collections.defaultdict[int, list[Tile]] = collections.defaultdict(list) + + for tile in self.tiles: + layer_index = 0 + while any((q, layer_index) in qubit_used for q in tile.data_set): + layer_index += 1 + qubit_used.update((q, layer_index) for q in tile.data_set) + layers[layer_index].append(tile) + return tuple(tuple(v) for _, v in sorted(layers.items())) + + def with_remaining_degrees_of_freedom_as_logicals(self) -> StabilizerCode: + """Solves for the logical observables, given only the stabilizers.""" + from stimflow._chunk import StabilizerCode + + return StabilizerCode(stabilizers=self).with_remaining_degrees_of_freedom_as_logicals() + + def with_edits(self, *, tiles: Iterable[Tile] | None = None) -> Patch: + return Patch(tiles=self.tiles if tiles is None else tiles) + + def with_transformed_coords(self, coord_transform: Callable[[complex], complex]) -> Patch: + return Patch([e.with_transformed_coords(coord_transform) for e in self.tiles]) + + def with_transformed_bases( + self, basis_transform: Callable[[Literal["X", "Y", "Z"]], Literal["X", "Y", "Z"]] + ) -> Patch: + return Patch([e.with_transformed_bases(basis_transform) for e in self.tiles]) + + def with_only_x_tiles(self) -> Patch: + return Patch([tile for tile in self.tiles if tile.basis == "X"]) + + def with_only_y_tiles(self) -> Patch: + return Patch([tile for tile in self.tiles if tile.basis == "Y"]) + + def with_only_z_tiles(self) -> Patch: + return Patch([tile for tile in self.tiles if tile.basis == "Z"]) + + @functools.cached_property + def m2tile(self) -> dict[complex, Tile]: + return {e.measure_qubit: e for e in self.tiles} + + def _repr_svg_(self) -> str: + return self.to_svg() + + def to_svg( + self, + *, + title: str | list[str] | None = None, + other: Patch | StabilizerCode | Iterable[Patch | StabilizerCode] = (), + show_order: bool = False, + show_measure_qubits: bool = False, + show_data_qubits: bool = True, + system_qubits: Iterable[complex] = (), + show_coords: bool = True, + opacity: float = 1, + show_obs: bool = False, + rows: int | None = None, + cols: int | None = None, + tile_color_func: Callable[[Tile], str] | None = None, + ) -> str_svg: + from stimflow._chunk._stabilizer_code import StabilizerCode + from stimflow._viz import svg + + patches = [self] + ([other] if isinstance(other, (Patch, StabilizerCode)) else list(other)) + + return svg( + objects=patches, + show_measure_qubits=show_measure_qubits, + show_data_qubits=show_data_qubits, + show_order=show_order, + system_qubits=system_qubits, + opacity=opacity, + show_coords=show_coords, + show_obs=show_obs, + rows=rows, + cols=cols, + tile_color_func=tile_color_func, + title=title, + ) + + def with_xz_flipped(self) -> Patch: + trans: dict[Literal["X", "Y", "Z"], Literal["X", "Y", "Z"]] = {"X": "Z", "Y": "Y", "Z": "X"} + return self.with_transformed_bases(trans.__getitem__) + + @functools.cached_property + def used_set(self) -> frozenset[complex]: + """Returns the set of all data and measure qubits used by tiles in the patch.""" + result: set[complex] = set() + for e in self.tiles: + result |= e.used_set + return frozenset(result) + + @functools.cached_property + def data_set(self) -> frozenset[complex]: + """Returns the set of all data qubits used by tiles in the patch.""" + result = set() + for e in self.tiles: + for q in e.data_qubits: + if q is not None: + result.add(q) + return frozenset(result) + + def __eq__(self, other): + if not isinstance(other, Patch): + return NotImplemented + return self.tiles == other.tiles + + def __ne__(self, other): + return not (self == other) + + @functools.cached_property + def measure_set(self) -> frozenset[complex]: + """Returns the set of all measure qubits used by tiles in the patch.""" + return frozenset(e.measure_qubit for e in self.tiles if e.measure_qubit is not None) + + def __add__(self, other: Patch) -> Patch: + if not isinstance(other, Patch): + return NotImplemented + return Patch([*self, *other]) + + def __repr__(self): + return "\n".join( + [ + "stimflow.Patch(tiles=[", + *[f" {e!r},".replace("\n", "\n ") for e in self.tiles], + "])", + ] + ) diff --git a/glue/stimflow/src/stimflow/_chunk/_patch_test.py b/glue/stimflow/src/stimflow/_chunk/_patch_test.py new file mode 100644 index 00000000..d1c524a0 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_patch_test.py @@ -0,0 +1,49 @@ +import stimflow + + +def test_to_svg(): + patch = stimflow.Patch( + [stimflow.Tile(bases="X", data_qubits=[0, 1, 1j, 1 + 1j], measure_qubit=0.5 + 0.5j)] + ) + assert ( + patch.to_svg() + == """ + + +0 +1 +0i +1i + + + + + + + + """.strip() # noqa: E501 + ) + + +def test_with_remaining_degrees_of_freedom_as_logicals(): + patch = stimflow.Patch([stimflow.PauliMap({"X": [0, 1, 2, 3]}), stimflow.PauliMap({"Z": [0, 1, 2, 3]})]) + code = patch.with_remaining_degrees_of_freedom_as_logicals() + assert code.stabilizers == patch + assert len(code.logicals) == 2 + code.verify() + assert code == stimflow.StabilizerCode( + stabilizers=[stimflow.PauliMap({"X": [0, 1, 2, 3]}), stimflow.PauliMap({"Z": [0, 1, 2, 3]})], + logicals=[ + # Not sure how stable the exact answer is. + (stimflow.PauliMap({"X": [1, 2]}, name="X1"), stimflow.PauliMap({"Z": [0, 2]}, name="Z1")), + (stimflow.PauliMap({"X": [1, 3]}, name="X2"), stimflow.PauliMap({"Z": [0, 3]}, name="Z2")), + ], + ) + + +def test_partitioned_tiles(): + t0 = stimflow.Tile(data_qubits=[0, 1, 2, 3], bases="X", flags={"A"}) + t1 = stimflow.Tile(data_qubits=[2, 3, 4, 5], bases="Z", flags={"B"}) + t2 = stimflow.Tile(data_qubits=[4, 5, 6, 7], bases="Y", flags={"C"}) + patch = stimflow.Patch([t0, t1, t2]) + assert patch.partitioned_tiles == ((t0, t2), (t1,)) diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py new file mode 100644 index 00000000..f174ce57 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py @@ -0,0 +1,744 @@ +from __future__ import annotations + +import collections +import functools +from collections.abc import Callable, Iterable, Sequence +from typing import Any, cast, Literal, TYPE_CHECKING + +import stim + +from stimflow._chunk._flow_metadata import FlowMetadata +from stimflow._chunk._patch import Patch +from stimflow._core import min_max_complex, NoiseRule, PauliMap, sorted_complex, str_svg, Tile + +if TYPE_CHECKING: + from stimflow._chunk._chunk_builder import ChunkBuilder + import stimflow + + +class StabilizerCode: + """This class stores the stabilizers and logicals of a stabilizer code. + + The exact semantics of the class are somewhat loose. For example, by default + this class doesn't verify that its fields actually form a valid stabilizer + code. This is so that the class can be used as a sort of useful data dumping + ground even in cases where what is being built isn't a stabilizer code. For + example, you can store a gauge code in the fields... it's just that methods + like 'make_code_capacity_circuit' will no longer work. + + The stabilizers are stored as a `stimflow.Patch`. A patch is like a list of `stimflow.PauliMap`, + except it actually stores `stimflow.Tile` instances so additional annotations can be added + and additional utility methods are easily available. + """ + + def __init__( + self, + stabilizers: Iterable[Tile | PauliMap] | Patch | None = None, + *, + logicals: Iterable[PauliMap | tuple[PauliMap, PauliMap]] = (), + scattered_logicals: Iterable[PauliMap] = (), + ): + """ + + Args: + stabilizers: The stabilizers of the code, specified as a Patch + logicals: The logical qubits and/or observables of the code. Each entry should be + either a pair of anti-commuting stimflow.PauliMap (e.g. the X and Z observables of the + logical qubit) or a single stimflow.PauliMap (e.g. just the X observable). + scattered_logicals: Logical operators with arbitrary commutation relationships to each + other. Still expected to commute with the stabilizers. + """ + __tracebackhide__ = True + packed_obs: list[PauliMap | tuple[PauliMap, PauliMap]] = [] + for obs in logicals: + if isinstance(obs, PauliMap): + packed_obs.append(obs) + elif len(obs) == 2 and isinstance(obs[0], PauliMap) and isinstance(obs[1], PauliMap): + packed_obs.append(cast(tuple[PauliMap, PauliMap], tuple(obs))) + else: + raise NotImplementedError( + f"{obs=} isn't a Pauli product or anti-commuting pair of Pauli products." + ) + if stabilizers is None: + stabilizers = Patch([]) + elif not isinstance(stabilizers, Patch): + stabilizers = Patch(stabilizers) + + self.stabilizers: Patch = stabilizers + self.logicals: tuple[PauliMap | tuple[PauliMap, PauliMap], ...] = tuple(packed_obs) + self.scattered_logicals: tuple[PauliMap, ...] = tuple(scattered_logicals) + + seen_names = set() + for obs in self.flat_logicals: + if obs.name is None: + raise ValueError(f"Unnamed logical operator: {obs!r}") + if obs.name in seen_names: + raise ValueError(f"Name collision {obs.name=}") + seen_names.add(obs.name) + + @property + def patch(self) -> Patch: + """Returns the stimflow.Patch storing the stabilizers of the code.""" + return self.stabilizers + + @functools.cached_property + def flat_logicals(self) -> tuple[PauliMap, ...]: + """Returns a list of the logical operators defined by the stabilizer code. + + It's "flat" because paired X/Z logicals are returned separately instead of + as a tuple. + """ + result: list[PauliMap] = [] + for logical in self.logicals: + if isinstance(logical, tuple): + result.extend(logical) + else: + result.append(logical) + result.extend(self.scattered_logicals) + return tuple(result) + + def with_remaining_degrees_of_freedom_as_logicals(self) -> StabilizerCode: + """Solves for the logical observables, given only the stabilizers.""" + + # Collect constraints. + gen_pauli_maps: list[PauliMap] = [] + for stabilizer in self.stabilizers: + gen_pauli_maps.append(stabilizer.to_pauli_map()) + for logical in self.logicals: + if isinstance(logical, tuple): + raise NotImplementedError( + "Logical (X, Z) pairs. Result might change the destabilizer." + ) + gen_pauli_maps.extend(self.flat_logicals) + + # Convert to stim types. + q2i = {q: i for i, q in enumerate(sorted_complex(self.patch.data_set))} + i2q = {i: q for q, i in q2i.items()} + stim_pauli_strings: list[stim.PauliString] = [] + for pm in gen_pauli_maps: + ps = stim.PauliString(len(q2i)) + for q, p in pm.items(): + ps[q2i[q]] = p + stim_pauli_strings.append(ps) + + # Solve remaining degrees of freedom with stim. + full_tableau = stim.Tableau.from_stabilizers( + stim_pauli_strings, allow_underconstrained=True, allow_redundant=True + ) + + # Convert back to stimflow. + new_logicals = [] + for k in range(len(full_tableau)): + z = full_tableau.z_output(k) + if z in stim_pauli_strings: + continue + x = full_tableau.x_output(k) + x2 = PauliMap(x).with_transformed_coords(cast(Any, i2q.__getitem__)) + z2 = PauliMap(z).with_transformed_coords(cast(Any, i2q.__getitem__)) + new_logicals.append((x2.with_name(f"inferred_X{k}"), z2.with_name(f"inferred_Z{k}"))) + + return StabilizerCode( + stabilizers=self.patch, + logicals=(*self.logicals, *new_logicals), + scattered_logicals=self.scattered_logicals, + ) + + def with_integer_coordinates(self) -> StabilizerCode: + """Returns an equivalent stabilizer code, but with all qubit on Gaussian integers.""" + + r2r = {v: i for i, v in enumerate(sorted({e.real for e in self.used_set}))} + i2i = {v: i for i, v in enumerate(sorted({e.imag for e in self.used_set}))} + return self.with_transformed_coords(lambda e: r2r[e.real] + i2i[e.imag] * 1j) + + def physical_to_logical(self, ps: stim.PauliString) -> PauliMap: + """Maps a physical qubit string into a logical qubit string. + + Requires that all logicals be specified as X/Z tuples. + """ + result: PauliMap = PauliMap() + for q in ps.pauli_indices(): + if q >= len(self.logicals): + raise ValueError("More qubits than logicals.") + obs = self.logicals[q] + if isinstance(obs, PauliMap): + raise ValueError( + "Need logicals to be pairs of observables to map physical to logical." + ) + p = ps[q] + if p == 1: + result *= obs[0] + elif p == 2: + result *= obs[0] + result *= obs[1] + elif p == 3: + result *= obs[1] + else: + assert False + return result + + def concat_over( + self, under: StabilizerCode, *, skip_inner_stabilizers: bool = False + ) -> StabilizerCode: + """Computes the interleaved concatenation of two stabilizer codes.""" + over = self.with_integer_coordinates() + c_min, c_max = min_max_complex(under.data_set) + pitch = c_max - c_min + 1 + 1j + + def concatenated_obs(over_obs: PauliMap, under_index: int) -> PauliMap: + total = PauliMap() + for q, p in over_obs.items(): + obs_pair = under.logicals[under_index] + if not isinstance(obs_pair, tuple): + raise NotImplementedError("Partial observable") + logical_x, logical_z = obs_pair + if p == "X": + obs = logical_x + elif p == "Y": + obs = logical_x * logical_z + elif p == "Z": + obs = logical_z + else: + raise NotImplementedError(f"{q=}, {p=}") + total *= obs.with_transformed_coords( + lambda e: q.real * pitch.real + q.imag * pitch.imag * 1j + e + ) + return total.with_name((over_obs.name, under_index)) + + new_stabilizers = [] + for stabilizer in over.stabilizers: + ps = stabilizer.to_pauli_map() + for k in range(len(under.logicals)): + new_stabilizers.append( + concatenated_obs(ps, k).to_tile().with_edits(flags=stabilizer.flags) + ) + if not skip_inner_stabilizers: + for stabilizer in under.stabilizers: + for q in over.data_set: + new_stabilizers.append( + stabilizer.with_transformed_coords( + lambda e: q.real * pitch.real + q.imag * pitch.imag * 1j + e + ) + ) + + new_logicals: list[PauliMap | tuple[PauliMap, PauliMap]] = [] + for logical in over.logicals: + for k in range(len(under.logicals)): + if isinstance(logical, PauliMap): + new_logicals.append(concatenated_obs(logical, k)) + else: + x, z = logical + new_logicals.append((concatenated_obs(x, k), concatenated_obs(z, k))) + + return StabilizerCode(stabilizers=new_stabilizers, logicals=new_logicals) + + def get_observable_by_basis( + self, index: int, basis: Literal["X", "Y", "Z"] | str, *, default: Any = "__!not_specified" + ) -> PauliMap: + obs = self.logicals[index] + if isinstance(obs, PauliMap) and set(obs.values()) == {basis}: + return obs + elif isinstance(obs, tuple): + a1, a2 = obs + b1 = frozenset(a1.values()) + b2 = frozenset(a2.values()) + if b1 == {basis}: + return a1 + if b2 == {basis}: + return a2 + if len(b1) == 1 and len(b2) == 1: + # For example, we have X and Z specified and the user asked for Y. + # Note that this works even if the X doesn't exactly overlap the Z. + return (a1 * a2).with_name((a1.name, a2.name)) + if default != "__!not_specified": + return default + raise ValueError(f"Couldn't return a basis {basis} observable from {obs=}.") + + def list_pure_basis_observables(self, basis: Literal["X", "Y", "Z"]) -> list[PauliMap]: + result = [] + for k in range(len(self.logicals)): + obs = self.get_observable_by_basis(k, basis, default=None) + if obs is not None: + result.append(obs) + return result + + def x_basis_subset(self) -> StabilizerCode: + return StabilizerCode( + stabilizers=self.stabilizers.with_only_x_tiles(), + logicals=self.list_pure_basis_observables("X"), + ) + + def z_basis_subset(self) -> StabilizerCode: + return StabilizerCode( + stabilizers=self.stabilizers.with_only_x_tiles(), + logicals=self.list_pure_basis_observables("Z"), + ) + + @property + def tiles(self) -> tuple[stimflow.Tile, ...]: + """Returns the tiles of the code's stabilizer patch.""" + return self.stabilizers.tiles + + def verify_distance_is_at_least_2(self): + """Verifies undetected logical errors require at least 2 physical errors. + + Verifies using a code capacity noise model. + """ + __tracebackhide__ = True + self.verify() + + circuit = self.make_code_capacity_circuit(noise=1e-3) + for inst in circuit.detector_error_model(): + if inst.type == "error": + dets = set() + obs = set() + for target in inst.targets_copy(): + if target.is_relative_detector_id(): + dets ^= {target.val} + elif target.is_logical_observable_id(): + obs ^= {target.val} + dets = frozenset(dets) + obs = frozenset(obs) + if obs and not dets: + filter_det = stim.DetectorErrorModel() + filter_det.append(inst) + err = circuit.explain_detector_error_model_errors(dem_filter=filter_det) + loc = err[0].circuit_error_locations[0].flipped_pauli_product[0] + raise ValueError( + f"Code has a distance 1 error:" + f"\n {loc.gate_target.pauli_type} at {loc.coords}" + ) + + def verify_distance_is_at_least_3(self): + """Verifies undetected logical errors require at least 3 physical errors. + + Verifies using a code capacity noise model. + """ + __tracebackhide__ = True + self.verify_distance_is_at_least_2() + seen = {} + circuit = self.make_code_capacity_circuit(noise=1e-3) + for inst in circuit.detector_error_model().flattened(): + if inst.type == "error": + dets = set() + obs = set() + for target in inst.targets_copy(): + if target.is_relative_detector_id(): + dets ^= {target.val} + elif target.is_logical_observable_id(): + obs ^= {target.val} + dets = frozenset(dets) + obs = frozenset(obs) + if dets not in seen: + seen[dets] = (obs, inst) + elif seen[dets][0] != obs: + filter_det = stim.DetectorErrorModel() + filter_det.append(inst) + filter_det.append(seen[dets][1]) + err = circuit.explain_detector_error_model_errors( + dem_filter=filter_det, reduce_to_one_representative_error=True + ) + loc1 = err[0].circuit_error_locations[0].flipped_pauli_product[0] + loc2 = err[1].circuit_error_locations[0].flipped_pauli_product[0] + raise ValueError( + f"Code has a distance 2 error:" + f"\n {loc1.gate_target.pauli_type} at {loc1.coords}" + f"\n {loc2.gate_target.pauli_type} at {loc2.coords}" + ) + + def find_distance(self, *, max_search_weight: int) -> int: + return len(self.find_logical_error(max_search_weight=max_search_weight)) + + def find_logical_error(self, *, max_search_weight: int) -> list[stim.ExplainedError]: + circuit = self.make_code_capacity_circuit(noise=1e-3) + if max_search_weight == 2: + return circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) + return circuit.search_for_undetectable_logical_errors( + dont_explore_edges_with_degree_above=max_search_weight, + dont_explore_detection_event_sets_with_size_above=max_search_weight, + dont_explore_edges_increasing_symptom_degree=False, + canonicalize_circuit_errors=True, + ) + + def with_observables_from_basis(self, basis: Literal["X", "Y", "Z"]) -> StabilizerCode: + if basis == "X": + return StabilizerCode( + stabilizers=self.stabilizers, logicals=self.list_pure_basis_observables("X") + ) + elif basis == "Y": + return StabilizerCode( + stabilizers=self.stabilizers, logicals=self.list_pure_basis_observables("Y") + ) + elif basis == "Z": + return StabilizerCode( + stabilizers=self.stabilizers, logicals=self.list_pure_basis_observables("Z") + ) + else: + raise NotImplementedError(f"{basis=}") + + def as_interface(self) -> stimflow.ChunkInterface: + from stimflow._chunk._chunk_interface import ChunkInterface + + ports: list[PauliMap] = [] + for tile in self.stabilizers.tiles: + if tile.data_set: + ports.append(tile.to_pauli_map()) + ports.extend(self.flat_logicals) + return ChunkInterface(ports=ports, discards=[]) + + def with_edits( + self, + *, + stabilizers: Iterable[Tile | PauliMap] | Patch | None = None, + logicals: Iterable[PauliMap | tuple[PauliMap, PauliMap]] | None = None, + ) -> StabilizerCode: + return StabilizerCode( + stabilizers=self.stabilizers if stabilizers is None else stabilizers, + logicals=self.logicals if logicals is None else logicals, + ) + + @functools.cached_property + def data_set(self) -> frozenset[complex]: + result = set(self.stabilizers.data_set) + for obs in self.logicals: + if isinstance(obs, PauliMap): + result |= obs.keys() + else: + a, b = obs + result |= a.keys() + result |= b.keys() + return frozenset(result) + + @functools.cached_property + def measure_set(self) -> frozenset[complex]: + return self.stabilizers.measure_set + + @functools.cached_property + def used_set(self) -> frozenset[complex]: + result = set(self.stabilizers.used_set) + for obs in self.logicals: + if isinstance(obs, PauliMap): + result |= obs.keys() + else: + a, b = obs + result |= a.keys() + result |= b.keys() + return frozenset(result) + + @staticmethod + def from_patch_with_inferred_observables(patch: Patch) -> StabilizerCode: + return StabilizerCode(patch).with_remaining_degrees_of_freedom_as_logicals() + + def verify(self) -> None: + """Verifies observables and stabilizers relate as a stabilizer code. + + All stabilizers should commute with each other. + All stabilizers should commute with all observables. + Same-index X and Z observables should anti-commute. + All other observable pairs should commute. + """ + __tracebackhide__ = True + + q2tiles: collections.defaultdict[complex, list[Tile]] = collections.defaultdict(list) + for tile in self.stabilizers.tiles: + for q in tile.data_set: + q2tiles[q].append(tile) + for tile1 in self.stabilizers.tiles: + overlapping = {tile2 for q in tile1.data_set for tile2 in q2tiles[q]} + for tile2 in overlapping: + t1 = tile1.to_pauli_map() + t2 = tile2.to_pauli_map() + if not t1.commutes(t2): + raise ValueError( + f"Tile stabilizer {t1=} anticommutes with tile stabilizer {t2=}." + ) + + for tile in self.stabilizers.tiles: + ps = tile.to_pauli_map() + for obs in self.flat_logicals: + if not ps.commutes(obs): + raise ValueError(f"Tile stabilizer {tile=} anticommutes with {obs=}.") + + for entry in self.logicals: + if not isinstance(entry, PauliMap): + a, b = entry + if a.commutes(b): + raise ValueError(f"The observable pair {a} vs {b} didn't anticommute.") + + packed_obs: list[Sequence[PauliMap]] = [] + for entry in self.logicals: + if isinstance(entry, PauliMap): + packed_obs.append([entry]) + else: + packed_obs.append(entry) + for k1 in range(len(packed_obs)): + for k2 in range(k1 + 1, len(packed_obs)): + for obs1 in packed_obs[k1]: + for obs2 in packed_obs[k2]: + if not obs1.commutes(obs2): + raise ValueError( + f"Unpaired observables didn't commute: {obs1=}, {obs2=}." + ) + + def with_xz_flipped(self) -> StabilizerCode: + """Returns the same stabilizer code, but with all qubits Hadamard conjugated.""" + new_observables: list[PauliMap | tuple[PauliMap, PauliMap]] = [] + for entry in self.logicals: + if isinstance(entry, PauliMap): + new_observables.append(entry.with_xz_flipped()) + else: + a, b = entry + new_observables.append((a.with_xz_flipped(), b.with_xz_flipped())) + return StabilizerCode( + stabilizers=self.stabilizers.with_xz_flipped(), logicals=new_observables + ) + + def _repr_svg_(self) -> str: + return self.to_svg() + + def to_svg( + self, + *, + title: str | list[str] | None = None, + canvas_height: int | None = None, + show_order: bool = False, + show_measure_qubits: bool = False, + show_data_qubits: bool = True, + system_qubits: Iterable[complex] = (), + opacity: float = 1, + show_coords: bool = True, + show_obs: bool = True, + other: stimflow.StabilizerCode | Patch | Iterable[stimflow.StabilizerCode | Patch] | None = None, + tile_color_func: ( + Callable[ + [stimflow.Tile], + str | tuple[float, float, float] | tuple[float, float, float, float] | None, + ] + | None + ) = None, + rows: int | None = None, + cols: int | None = None, + find_logical_err_max_weight: int | None = None, + stabilizer_style: Literal["polygon", "circles"] | None = "polygon", + observable_style: Literal["label", "polygon", "circles"] = "label", + ) -> str_svg: + """Returns an SVG diagram of the stabilizer code.""" + flat: list[StabilizerCode | Patch] = [self] if self is not None else [] + if isinstance(other, (StabilizerCode, Patch)): + flat.append(other) + elif other is not None: + flat.extend(other) + + from stimflow._viz import svg + + return svg( + objects=flat, + title=title, + show_obs=show_obs, + canvas_height=canvas_height, + show_measure_qubits=show_measure_qubits, + show_data_qubits=show_data_qubits, + show_order=show_order, + find_logical_err_max_weight=find_logical_err_max_weight, + system_qubits=system_qubits, + opacity=opacity, + show_coords=show_coords, + tile_color_func=tile_color_func, + cols=cols, + rows=rows, + stabilizer_style=stabilizer_style, + observable_style=observable_style, + ) + + def with_transformed_coords( + self, coord_transform: Callable[[complex], complex] + ) -> StabilizerCode: + """Returns the same stabilizer code, but with coordinates transformed by the given + function.""" + new_observables: list[PauliMap | tuple[PauliMap, PauliMap]] = [] + for entry in self.logicals: + if isinstance(entry, PauliMap): + new_observables.append(entry.with_transformed_coords(coord_transform)) + else: + a, b = entry + new_observables.append( + ( + a.with_transformed_coords(coord_transform), + b.with_transformed_coords(coord_transform), + ) + ) + return StabilizerCode( + stabilizers=self.stabilizers.with_transformed_coords(coord_transform), + logicals=new_observables, + scattered_logicals=[ + e.with_transformed_coords(coord_transform) for e in self.scattered_logicals + ], + ) + + def make_code_capacity_circuit( + self, + *, + noise: float | NoiseRule, + metadata_func: Callable[[stimflow.Flow], stimflow.FlowMetadata] = lambda _: FlowMetadata(), + ) -> stim.Circuit: + """Produces a code capacity noisy memory experiment circuit for the stabilizer code.""" + if isinstance(noise, (int, float)): + noise = NoiseRule(after={"DEPOLARIZE1": noise}) + if noise.flip_result: + raise ValueError(f"{noise=} includes measurement noise.") + from stimflow._chunk._chunk_compiler import ChunkCompiler + + compiler = ChunkCompiler(metadata_func=metadata_func) + interface = self.as_interface() + compiler.append_magic_init_chunk(interface) + all_qs = sorted(compiler.q2i.values()) + for gate, strength in noise.before.items(): + compiler.circuit.append(gate, targets=all_qs, arg=[strength]) + for gate, strength in noise.after.items(): + compiler.circuit.append(gate, targets=all_qs, arg=[strength]) + compiler.append_magic_end_chunk(interface) + return compiler.finish_circuit() + + def make_phenom_circuit( + self, + *, + noise: float | NoiseRule, + rounds: int, + metadata_func: Callable[[stimflow.Flow], stimflow.FlowMetadata] = lambda _: FlowMetadata(), + ) -> stim.Circuit: + """Produces a phenomenological noise memory experiment circuit for the stabilizer code.""" + if isinstance(noise, (int, float)): + noise = NoiseRule(after={"DEPOLARIZE1": noise}, flip_result=noise) + from stimflow._chunk._chunk_compiler import ChunkCompiler + from stimflow._chunk._chunk_loop import ChunkLoop + + from stimflow._chunk._chunk_builder import ChunkBuilder + builder = ChunkBuilder(self.data_set) + for obs in self.flat_logicals: + builder.add_flow(start=obs, end=obs) + for gate, strength in noise.before.items(): + builder.append(gate, self.data_set, arg=strength) + for k, tile in enumerate(self.tiles): + builder.add_flow(start=tile, end=tile) + before_noise_chunk = builder.finish_chunk(wants_to_merge_with_next=True) + + builder = ChunkBuilder(self.data_set) + for obs in self.flat_logicals: + builder.add_flow(start=obs, end=obs) + for gate, strength in noise.after.items(): + builder.append(gate, self.data_set, arg=strength) + for k, tile in enumerate(self.tiles): + builder.add_flow(start=tile, end=tile) + after_noise_chunk = builder.finish_chunk(wants_to_merge_with_prev=True) + + builder = ChunkBuilder(self.data_set) + for obs in self.flat_logicals: + builder.add_flow(start=obs, end=obs) + for k1, layer in enumerate(self.patch.partitioned_tiles): + if k1 > 0: + builder.append("TICK") + for k2, tile in enumerate(layer): + builder.append( + "MPP", [tile], measure_key_func=lambda _: f"det{k1},{k2}", arg=noise.flip_result + ) + builder.add_flow(end=tile, ms=[f"det{k1},{k2}"]) + builder.add_flow(start=tile, ms=[f"det{k1},{k2}"]) + + measure_chunk = builder.finish_chunk() + + compiler = ChunkCompiler(metadata_func=metadata_func) + compiler.append_magic_init_chunk(measure_chunk.start_interface()) + compiler.append(before_noise_chunk.with_edits(wants_to_merge_with_next=False)) + compiler.append(ChunkLoop([before_noise_chunk, measure_chunk, after_noise_chunk], rounds)) + compiler.append(after_noise_chunk.with_edits(wants_to_merge_with_prev=False)) + compiler.append_magic_end_chunk() + return compiler.finish_circuit() + + def __repr__(self) -> str: + def indented(x: str) -> str: + return x.replace("\n", "\n ") + + def indented_repr(x: Any) -> str: + if isinstance(x, tuple): + return indented(indented("[\n" + ",\n".join(indented_repr(e) for e in x)) + ",\n]") + return indented(repr(x)) + + return f"""stimflow.StabilizerCode( + stabilizers={indented_repr(self.stabilizers)}, + logicals={indented_repr(self.logicals)}, + scattered_logicals={indented_repr(self.scattered_logicals)}, +)""" + + def __eq__(self, other) -> bool: + if not isinstance(other, StabilizerCode): + return NotImplemented + return self.stabilizers == other.stabilizers and self.logicals == other.logicals + + def __ne__(self, other) -> bool: + return not (self == other) + + @functools.lru_cache(maxsize=1) + def __hash__(self) -> int: + return hash((StabilizerCode, self.stabilizers, self.logicals)) + + def transversal_init_chunk( + self, + *, + basis: ( + Literal["X", "Y", "Z"] + | str + | stimflow.PauliMap + | dict[complex, str | Literal["X", "Y", "Z"]] + ), + ) -> stimflow.Chunk: + """Returns a chunk that describes initializing the stabilizer code with given reset bases. + + Stabilizers that anticommute with the resets will be discarded flows. + + The returned chunk isn't guaranteed to be fault tolerant. + """ + from stimflow._chunk._chunk_builder import ChunkBuilder + + basis_map: PauliMap + if isinstance(basis, str): + basis_map = PauliMap({cast(Any, basis): self.data_set}) + elif isinstance(basis, PauliMap): + basis_map = basis + elif isinstance(basis, dict): + basis_map = PauliMap(basis) + else: + raise NotImplementedError(f"{basis=}") + builder = ChunkBuilder(self.data_set) + for b in "XYZ": + builder.append(f"R{b}", [q for q in self.data_set if basis_map[q] == b]) + for tile in self.tiles: + if not tile.data_set: + continue + if all(basis_map[q] == p for q, p in tile.to_pauli_map().items()): + builder.add_flow(end=tile) + else: + builder.add_discarded_flow_output(tile) + for obs in self.flat_logicals: + if all(basis_map[q] == p for q, p in obs.items()): + builder.add_flow(end=obs) + else: + builder.add_discarded_flow_output(obs) + + return builder.finish_chunk(wants_to_merge_with_next=True) + + def transversal_measure_chunk( + self, + *, + basis: ( + Literal["X", "Y", "Z"] + | str + | stimflow.PauliMap + | dict[complex, str | Literal["X", "Y", "Z"]] + ), + ) -> stimflow.Chunk: + """Returns a chunk that describes measuring the stabilizer code with given measure bases. + + Stabilizers that anticommute with the measurements will be discarded flows. + + The returned chunk isn't guaranteed to be fault tolerant. + """ + return self.transversal_init_chunk(basis=basis).time_reversed() diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py new file mode 100644 index 00000000..9c157c02 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py @@ -0,0 +1,314 @@ +import pytest +import stim + +import stimflow + + +def test_make_phenom_circuit_for_stabilizer_code(): + patch = stimflow.Patch( + [ + stimflow.Tile(bases="Z", data_qubits=[0, 1, 1j, 1 + 1j], measure_qubit=0.5 + 0.5j), + stimflow.Tile(bases="X", data_qubits=[0, 1], measure_qubit=0.5), + stimflow.Tile(bases="X", data_qubits=[0 + 1j, 1 + 1j], measure_qubit=0.5 + 1j), + ] + ) + obs_x = stimflow.PauliMap({0: "X", 1j: "X"}).with_name("LX") + obs_z = stimflow.PauliMap({0: "Z", 1: "Z"}).with_name("LZ") + + assert stimflow.StabilizerCode(stabilizers=patch, logicals=[(obs_x, obs_z)]).make_phenom_circuit( + noise=stimflow.NoiseRule(flip_result=0.125, after={"DEPOLARIZE1": 0.25}), + rounds=100, + metadata_func=lambda flow: stimflow.FlowMetadata(tag=(flow.obs_key or "") + "A"), + ) == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1, 1) 3 + OBSERVABLE_INCLUDE[LXA](0) X0 X1 + TICK + OBSERVABLE_INCLUDE[LZA](1) Z0 Z2 + TICK + MPP X0*X2 X1*X3 + TICK + MPP Z0*Z1*Z2*Z3 + TICK + REPEAT 100 { + MPP(0.125) X0*X2 X1*X3 + TICK + MPP(0.125) Z0*Z1*Z2*Z3 + DETECTOR[A](0.5, 0, 0) rec[-6] rec[-3] + DETECTOR[A](0.5, 1, 0) rec[-5] rec[-2] + DETECTOR[A](0.5, 0.5, 0) rec[-4] rec[-1] + SHIFT_COORDS(0, 0, 1) + DEPOLARIZE1(0.25) 0 1 2 3 + TICK + } + DEPOLARIZE1(0.25) 0 1 2 3 + TICK + MPP X0*X2 X1*X3 + TICK + MPP Z0*Z1*Z2*Z3 + DETECTOR[A](0.5, 0, 0) rec[-6] rec[-3] + DETECTOR[A](0.5, 1, 0) rec[-5] rec[-2] + DETECTOR[A](0.5, 0.5, 0) rec[-4] rec[-1] + TICK + OBSERVABLE_INCLUDE[LXA](0) X0 X1 + TICK + OBSERVABLE_INCLUDE[LZA](1) Z0 Z2 + """ + ) + + +def test_make_code_capacity_circuit_for_stabilizer_code(): + patch = stimflow.Patch( + [ + stimflow.Tile(bases="Z", data_qubits=[0, 1, 1j, 1 + 1j], measure_qubit=0.5 + 0.5j), + stimflow.Tile(bases="X", data_qubits=[0, 1], measure_qubit=0.5), + stimflow.Tile(bases="X", data_qubits=[0 + 1j, 1 + 1j], measure_qubit=0.5 + 1j), + ] + ) + obs_x = stimflow.PauliMap({0: "X", 1j: "X"}).with_name("LX") + obs_z = stimflow.PauliMap({0: "Z", 1: "Z"}).with_name("LZ") + + assert stimflow.StabilizerCode( + stabilizers=patch, logicals=[(obs_x, obs_z)] + ).make_code_capacity_circuit( + noise=stimflow.NoiseRule(after={"DEPOLARIZE1": 0.25}), + metadata_func=lambda flow: stimflow.FlowMetadata(tag=(flow.obs_key or "") + "B"), + ) == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1, 1) 3 + OBSERVABLE_INCLUDE[LXB](0) X0 X1 + TICK + OBSERVABLE_INCLUDE[LZB](1) Z0 Z2 + TICK + MPP X0*X2 X1*X3 + TICK + MPP Z0*Z1*Z2*Z3 + TICK + DEPOLARIZE1(0.25) 0 1 2 3 + TICK + MPP X0*X2 X1*X3 + TICK + MPP Z0*Z1*Z2*Z3 + DETECTOR[B](0.5, 0, 0) rec[-6] rec[-3] + DETECTOR[B](0.5, 1, 0) rec[-5] rec[-2] + DETECTOR[B](0.5, 0.5, 0) rec[-4] rec[-1] + TICK + OBSERVABLE_INCLUDE[LXB](0) X0 X1 + TICK + OBSERVABLE_INCLUDE[LZB](1) Z0 Z2 + """ + ) + + +def test_from_patch_with_inferred_observables(): + code = stimflow.StabilizerCode.from_patch_with_inferred_observables( + stimflow.Patch( + [ + stimflow.Tile(bases="XZZX", data_qubits=[0, 1, 2, 3], measure_qubit=0), + stimflow.Tile(bases="XZZX", data_qubits=[1, 2, 3, 4], measure_qubit=1), + stimflow.Tile(bases="XZZX", data_qubits=[2, 3, 4, 0], measure_qubit=2), + stimflow.Tile(bases="XZZX", data_qubits=[3, 4, 0, 1], measure_qubit=3), + ] + ) + ) + code.verify() + assert len(code.logicals) == 1 + assert len(code.logicals[0]) == 2 + + +def test_verify_distance_is_at_least_3(): + distance_1_code = stimflow.StabilizerCode( + stabilizers=stimflow.Patch([stimflow.Tile(bases="XXXX", data_qubits=[0, 1, 2, 3])]), + logicals=[ + ( + stimflow.PauliMap.from_xs([0, 1]).with_name("LX"), + stimflow.PauliMap.from_zs([0, 2]).with_name("LZ"), + ) + ], + ) + with pytest.raises(ValueError, match="distance 1 error"): + distance_1_code.verify_distance_is_at_least_2() + with pytest.raises(ValueError, match="distance 1 error"): + distance_1_code.verify_distance_is_at_least_3() + + distance_2_code = stimflow.StabilizerCode( + stabilizers=stimflow.Patch( + [ + stimflow.Tile(bases="XXXX", data_qubits=[0, 1, 2, 3]), + stimflow.Tile(bases="ZZZZ", data_qubits=[0, 1, 2, 3]), + ] + ), + logicals=[ + ( + stimflow.PauliMap.from_xs([0, 1]).with_name("LX"), + stimflow.PauliMap.from_zs([0, 2]).with_name("LZ"), + ) + ], + ) + distance_2_code.verify_distance_is_at_least_2() + with pytest.raises(ValueError, match="distance 2 error"): + distance_2_code.verify_distance_is_at_least_3() + + perfect_code = stimflow.StabilizerCode( + stabilizers=stimflow.Patch( + [ + stimflow.Tile(bases="XZZX", data_qubits=[0, 1, 2, 3]), + stimflow.Tile(bases="XZZX", data_qubits=[1, 2, 3, 4]), + stimflow.Tile(bases="XZZX", data_qubits=[2, 3, 4, 0]), + stimflow.Tile(bases="XZZX", data_qubits=[3, 4, 0, 1]), + ] + ), + logicals=[ + ( + stimflow.PauliMap.from_xs([0, 1, 2, 3, 4]).with_name("LX"), + stimflow.PauliMap.from_zs([0, 1, 2, 3, 4]).with_name("LZ"), + ) + ], + ) + perfect_code.verify_distance_is_at_least_2() + perfect_code.verify_distance_is_at_least_3() + + +def test_with_integer_coordinates(): + code = stimflow.StabilizerCode( + stabilizers=[ + stimflow.Tile(bases="XXXX", data_qubits=[0, 1, 1j, 1 + 1j], measure_qubit=1.5 + 0.5j), + stimflow.Tile(bases="ZZZZ", data_qubits=[0, 1, 1j, 1 + 1j]), + ], + logicals=[ + ( + stimflow.PauliMap.from_xs([0, 1]).with_name("LX1"), + stimflow.PauliMap.from_zs([0, 1j]).with_name("LZ1"), + ), + ( + stimflow.PauliMap.from_xs([0, 1j]).with_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_name("LZ2"), + ), + ], + ) + code.verify() + code2 = code.with_integer_coordinates() + assert code2 == stimflow.StabilizerCode( + stabilizers=[ + stimflow.Tile(bases="XXXX", data_qubits=[0, 1, 2j, 1 + 2j], measure_qubit=2 + 1j), + stimflow.Tile(bases="ZZZZ", data_qubits=[0, 1, 2j, 1 + 2j]), + ], + logicals=[ + ( + stimflow.PauliMap.from_xs([0, 1]).with_name("LX1"), + stimflow.PauliMap.from_zs([0, 2j]).with_name("LZ1"), + ), + ( + stimflow.PauliMap.from_xs([0, 2j]).with_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_name("LZ2"), + ), + ], + ) + + +def test_physical_to_logical(): + code = stimflow.StabilizerCode( + stabilizers=[ + stimflow.Tile(bases="XXXX", data_qubits=[0, 1, 1j, 1 + 1j], measure_qubit=1.5 + 0.5j), + stimflow.Tile(bases="ZZZZ", data_qubits=[0, 1, 1j, 1 + 1j]), + ], + logicals=[ + ( + stimflow.PauliMap.from_xs([0, 1]).with_name("LX1"), + stimflow.PauliMap.from_zs([0, 1j]).with_name("LZ1"), + ), + ( + stimflow.PauliMap.from_xs([0, 1j]).with_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_name("LZ2"), + ), + ], + ) + assert code.physical_to_logical(stim.PauliString("__")) == stimflow.PauliMap() + assert code.physical_to_logical(stim.PauliString("X_")) == stimflow.PauliMap({"X": [0, 1]}) + assert code.physical_to_logical(stim.PauliString("_X")) == stimflow.PauliMap({"X": [0, 1j]}) + assert code.physical_to_logical(stim.PauliString("XX")) == stimflow.PauliMap({"X": [1, 1j]}) + assert code.physical_to_logical(stim.PauliString("Z_")) == stimflow.PauliMap({"Z": [0, 1j]}) + assert code.physical_to_logical(stim.PauliString("_Z")) == stimflow.PauliMap({"Z": [0, 1]}) + assert code.physical_to_logical(stim.PauliString("ZZ")) == stimflow.PauliMap({"Z": [1, 1j]}) + assert code.physical_to_logical(stim.PauliString("Y_")) == stimflow.PauliMap( + {0: "Y", 1: "X", 1j: "Z"} + ) + assert code.physical_to_logical(stim.PauliString("_Y")) == stimflow.PauliMap( + {0: "Y", 1: "Z", 1j: "X"} + ) + assert code.physical_to_logical(stim.PauliString("YY")) == stimflow.PauliMap({1: "Y", 1j: "Y"}) + assert code.physical_to_logical(stim.PauliString("XZ")) == stimflow.PauliMap({0: "Y", 1: "Y"}) + + +def test_concat_over(): + a, b, c, d = [0, 1, 1j, 1 + 1j] + code = stimflow.StabilizerCode( + stabilizers=[stimflow.PauliMap.from_xs([a, b, c, d]), stimflow.PauliMap.from_zs([a, b, c, d])], + logicals=[ + ( + stimflow.PauliMap.from_xs([a, b]).with_name("LX1"), + stimflow.PauliMap.from_zs([a, c]).with_name("LX2"), + ), + ( + stimflow.PauliMap.from_zs([a, b]).with_name("LZ1"), + stimflow.PauliMap.from_xs([a, c]).with_name("LZ2"), + ), + ], + ) + code.verify() + code2 = code.concat_over(code) + code2.verify() + assert code2.find_distance(max_search_weight=8) == 4 + assert len(code2.logicals) == len(code.logicals) * len(code.logicals) + assert len(code2.stabilizers) == len(code.stabilizers) * len(code.logicals) + len( + code.stabilizers + ) * len(code.data_set) + assert len(code2.data_set) == len(code.data_set) * len(code.data_set) + + +def test_to_svg(): + a, b, c, d = [0, 1, 1j, 1 + 1j] + code = stimflow.StabilizerCode( + stabilizers=[stimflow.PauliMap.from_xs([a, b, c, d]), stimflow.PauliMap.from_zs([a, b, c, d])], + logicals=[ + ( + stimflow.PauliMap.from_xs([a, b]).with_name("LX1"), + stimflow.PauliMap.from_zs([a, c]).with_name("LZ1"), + ), + ( + stimflow.PauliMap.from_zs([a, b]).with_name("LX2"), + stimflow.PauliMap.from_xs([a, c]).with_name("LZ2"), + ), + ], + ) + assert isinstance(code.to_svg(), stimflow.str_svg) + + +def test_with_remaining_degrees_of_freedom_as_logicals(): + code = stimflow.StabilizerCode( + [stimflow.PauliMap({"X": [0, 1, 2, 3]}), stimflow.PauliMap({"Z": [0, 1, 2, 3]})] + ) + finished = code.with_remaining_degrees_of_freedom_as_logicals() + assert finished.stabilizers == code.stabilizers + assert len(finished.logicals) == 2 + finished.verify() + assert finished == stimflow.StabilizerCode( + stabilizers=[stimflow.PauliMap({"X": [0, 1, 2, 3]}), stimflow.PauliMap({"Z": [0, 1, 2, 3]})], + logicals=[ + # Not sure how stable the exact answer is. + ( + stimflow.PauliMap({"X": [1, 2]}).with_name("inferred_X0"), + stimflow.PauliMap({"Z": [0, 2]}).with_name("inferred_Z0"), + ), + ( + stimflow.PauliMap({"X": [1, 3]}).with_name("inferred_X1"), + stimflow.PauliMap({"Z": [0, 3]}).with_name("inferred_Z1"), + ), + ], + ) diff --git a/glue/stimflow/src/stimflow/_chunk/_test_util.py b/glue/stimflow/src/stimflow/_chunk/_test_util.py new file mode 100644 index 00000000..bac4fd37 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_test_util.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from collections.abc import Iterable + + +def assert_has_same_set_of_items_as( + actual: Iterable, + expected: Iterable, + actual_name: str = "actual", + expected_name: str = "expected", +) -> None: + __tracebackhide__ = True + + actual = frozenset(actual) + expected = frozenset(expected) + if actual == expected: + return + + lines = [f"set({actual_name}) != set({expected_name})", ""] + if actual - expected: + lines.append(f"Extra items in {actual_name} vs {expected_name}:") + for d in sorted(actual - expected): + lines.append(f" {d}") + if expected - actual: + lines.append(f"Missing items from {actual_name} vs {expected_name}:") + for d in sorted(expected - actual): + lines.append(f" {d}") + raise AssertionError("\n".join(lines)) diff --git a/glue/stimflow/src/stimflow/_chunk/_weave.py b/glue/stimflow/src/stimflow/_chunk/_weave.py new file mode 100644 index 00000000..f682ed1f --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_weave.py @@ -0,0 +1,370 @@ +import itertools +from collections.abc import Callable, Generator, Iterable, Iterator +from typing import TypeVar + +import stim + +T = TypeVar("T") + + +def pairs(iterable: Iterable[T]) -> Iterator[tuple[T, T]]: + prev = None + has_prev = False + for e in iterable: + if has_prev: + yield prev, e + else: + prev = e + has_prev ^= True + + +class StimCircuitLoom: + """Class for combining stim circuits running in parallel at separate locations. + + for standard usage, call StimCircuitLoom.weave(...), which returns the weaved circuit + for usage details, see the docstring to that function + + for complex usage, you can instantiate a loom StimCircuitLoom(...) + This is lets you access details of the weaving afterward, such as the measurement mapping + """ + + NON_MATCHING_INSTRUCTIONS = ["DETECTOR", "OBSERVABLE_INCLUDE", "QUBIT_COORDS", "SHIFT_COORDS"] + + @classmethod + def weave( + cls, + c0: stim.Circuit, + c1: stim.Circuit, + sweep_bit_func: Callable[[int, int], int] | None = None, + ) -> stim.Circuit: + """Combines two stim circuits instruction by instruction. + + Example usage: + StimCircuitLoom.weave(circuit_0, circuit_1) -> stim.Circuit + + Expects that the input circuit have 'matching instructions', in that they + contain exactly the same sequence of instructions which can be matched up + 1-to-1. This may require one circuit to have instructions with no targets, + purely to match instructions in the other circuit. Exceptions to this are + the annotation instructions DETECTOR, OBSERVABLE_INCLUDE, QUBIT_COORDS, + and SHIFT_COORDS, which do not need a matching statement in the other + circuit. This may not be what you want, as it will produce duplicate + DETECTOR or QUBIT_COORD instructions if they are included in both circuits. + The annotation TICK is considered a matching instruction. + + Generally, instructions are combined by placing all targets from the + first circuit instruction, followed by all targets from the second. + + In most gates, if a gate target is present in the first instruction + target list, it is removed from the second instructions target list. + As such, we do not permit instructions in the input circuits to have + duplicate targets. This avoids the ambiguity of deciding whether one + or both duplicates between circuits have to match up. + + Measure record targets are adjusted to point to the correct record in the + combined circuit e.g. DETECTOR rec[-1] or CX rec[-1] 1 + + Sweep bits are not handled by default, and will produce a ValueError. + If sweep_bit_func is provided, it will be used to produce new sweep bit + targets as follows: + new_sweep_bit_index = sweep_bit_func(circuit_index, sweep_bit_index) + where: + circuit_index = 0 for circuit_0 and 1 for circuit_1 + sweep_bit_index is the sweep bit index used in the input circuit + """ + return cls(c0, c1, sweep_bit_func).circuit + + def __init__( + self, + c0: stim.Circuit, + c1: stim.Circuit, + sweep_bit_func: Callable[[int, int], int] | None = None, + ): + self.circuit = stim.Circuit() + self.sweep_bit_func = sweep_bit_func + + self._c0_global_meas_idxs: list[int] = [] + self._c1_global_meas_idxs: list[int] = [] + + self._num_global_meas: int = 0 + # this isn't necessarily just the sum of meas in each sub-circuit + # (because we could have combined measurements) + # or the number of measurements actually added to self.circuit + # (because we could be halfway through processing an M instruction) + + self._c0_iter = enumerate(iter(c0)) + self._c1_iter = enumerate(iter(c1)) + + self._weave() + + # PUBLIC INTERFACES + + def weaved_target_rec_from_c0(self, target_rec: int) -> int: + """given a target rec in circuit_0, return the equiv rec in the weaved circuit. + + args: + target_rec: a valid measurement record target in the input circuit + follows python indexing semantics: + can be either positive (counting from the start of the circuit, 0 indexed) + or negative (counting from the end backwards, last measurement is [-1]) + The second is compatible with stim instruction target rec values + + returns: + The same measurements target rec in the weaved circuit. + Always returns a negative 'lookback' compatible with a stim circuit + Add StimCircuitWeave.circuit.num_measurements for an absolute measurement index + """ + return self._global_lookback( + local_lookback=target_rec, global_meas_idxs=self._c0_global_meas_idxs + ) + + def weaved_target_rec_from_c1(self, target_rec: int) -> int: + """given a target rec in circuit_1, return the equiv rec in the weaved circuit.""" + return self._global_lookback( + local_lookback=target_rec, global_meas_idxs=self._c1_global_meas_idxs + ) + + # PRIVATE METHODS + + def _add_c0_measurement(self): + self._c0_global_meas_idxs.append(self._num_global_meas) + self._num_global_meas += 1 + + def _add_c1_measurement(self): + self._c1_global_meas_idxs.append(self._num_global_meas) + self._num_global_meas += 1 + + def _dedup_c0_measurement(self, lookback_from_current_state: int): + # for when a c0 meas target duplicates a c0 meas target + self._c0_global_meas_idxs.append(self._num_global_meas + lookback_from_current_state) + # don't increment num_global_meas + + def _dedup_c1_measurement(self, lookback_from_current_state: int): + # for when a c1 meas target duplicates a c1 or c0 meas target + self._c1_global_meas_idxs.append(self._num_global_meas + lookback_from_current_state) + # don't increment num_global_meas + + def _global_lookback(self, local_lookback: int, global_meas_idxs: list[int]) -> int: + """computes meas rec lookbacks in the combined circuit from ones in the local circuit.""" + return global_meas_idxs[local_lookback] - self._num_global_meas + + def _matching_instructions_generator( + self, circuit_iter: Iterator, global_meas_idxs: list[int] + ) -> Generator[tuple[int, stim.CircuitInstruction]]: + while True: + try: + i, op = next(circuit_iter) + except StopIteration: + return # ends the generator + if op.name in self.NON_MATCHING_INSTRUCTIONS: + self._handle_non_matching_operations(op=op, global_meas_idxs=global_meas_idxs) + else: + yield i, op + + def _weave(self): + # we use generators so that we can only handle the matching case here: + # the generators handle the nonmatching case internally + + c0_gen = self._matching_instructions_generator(self._c0_iter, self._c0_global_meas_idxs) + c1_gen = self._matching_instructions_generator(self._c1_iter, self._c1_global_meas_idxs) + + for (i, op0), (j, op1) in zip(c0_gen, c1_gen): + + if op0.name != op1.name: + raise ValueError(f"Mismatched ops at position {i}: {op0}, {j}: {op1}") + + if op0.gate_args_copy() != op1.gate_args_copy(): + raise ValueError( + "Mismatched op arguments at position " + f"{i}: {op0}({op0.gate_args_copy()}), {j}: {op1}({op1.gate_args_copy()})" + ) + + self._handle_matching_operations(op0, op1) + + # Make sure both generators are done + # The fetch here is also what handles any trailing nonmatching instructions + for i, op0 in c0_gen: + raise ValueError(f"Unmatched operation in c0 {i}:{op0}") + for j, op1 in c1_gen: + raise ValueError(f"Unmatched operation in c1 {j}:{op1}") + + def _handle_matching_operations( + self, op0: stim.CircuitInstruction, op1: stim.CircuitInstruction + ): + + gd = stim.gate_data(op0.name) + if gd.produces_measurements: + if gd.is_single_qubit_gate: + self._handle_sq_m_gates(op0, op1) + elif gd.is_two_qubit_gate: + raise NotImplementedError("multiqubit measurement are not supported") + elif gd.takes_pauli_targets: + raise NotImplementedError("arbitrary pauli measurements are not supported") + elif op0.name == "MPAD": + raise NotImplementedError("MPAD not supported") + else: + raise ValueError(f"Unrecognised measurement operation {op0.name}") + else: + if op0.name == "TICK": + self.circuit.append("TICK") + elif gd.is_single_qubit_gate: + self._handle_sq_u_gates(op0, op1) + elif gd.is_two_qubit_gate: + self._handle_2q_u_gates(op0, op1) + elif gd.takes_pauli_targets: + raise NotImplementedError("arbitrary pauli gates are not supported") + else: + raise ValueError(f"Unrecognised operation {op0.name}") + + def _handle_sq_m_gates(self, op0: stim.CircuitInstruction, op1: stim.CircuitInstruction): + + op0_targets = op0.targets_copy() + op1_targets = op1.targets_copy() + + targets = [] + + for t in op0_targets: + if t not in targets: + targets.append(t) + self._add_c0_measurement() + else: + raise ValueError(f"Duplicate gate target {t} in c0:{op0}") + + for ti, t in enumerate(op1_targets): + if t not in targets: + targets.append(t) + self._add_c1_measurement() + elif t in op1_targets[:ti]: + raise ValueError(f"Duplicate gate target {t} in c0:{op0}") + else: + lookback = targets.index(t) - len(targets) # lookback is -ve + self._dedup_c1_measurement(lookback) + + self.circuit.append(name=op0.name, targets=targets, arg=op0.gate_args_copy()) + + def _handle_sq_u_gates(self, op0: stim.CircuitInstruction, op1: stim.CircuitInstruction): + # easy mode, dedup targets, + op0_targets = op0.targets_copy() + op1_targets = op1.targets_copy() + + targets = [] + for t in op0_targets: + if t not in targets: + targets.append(t) + else: + raise ValueError(f"Duplicate target {t} in SQ gate {op0} in circuit 0") + for ti, t in enumerate(op1_targets): + if t not in targets: + targets.append(t) + elif t in op1_targets[:ti]: + raise ValueError(f"Duplicate target {t} in SQ gate {op1} in circuit 1") + # otherwise it's in there already, leave it be + + self.circuit.append(name=op0.name, targets=targets, arg=op0.gate_args_copy()) + + def _handle_2q_u_gates(self, op0: stim.CircuitInstruction, op1: stim.CircuitInstruction): + # combine the targets in pairs, also check for rec or sweep targets + op0_targets = op0.targets_copy() + op1_targets = op1.targets_copy() + + touched_qubit_targets = set() + target_pairs: list[tuple[stim.GateTarget, stim.GateTarget]] = [] + + # OP0 TARGETS + for pair in pairs(op0_targets): + if pair in target_pairs: + raise ValueError(f"Duplicate target pair {pair} in 2Q gate {op0} in circuit 0") + if pair[::-1] in target_pairs: + raise ValueError( + f"Duplicate reversed target pair {pair}/{pair[::-1]} " + f"in 2Q gate {op0} in circuit 0" + ) + + new_pair = [] + for t in pair: + if t.is_qubit_target: + if t in touched_qubit_targets: + raise ValueError(f"Duplicate target {t} in 2Q gate {op0} in circuit 0") + touched_qubit_targets.add(t) + new_t = t + elif t.is_measurement_record_target: + new_t = stim.target_rec( + self._global_lookback( + local_lookback=t.value, global_meas_idxs=self._c0_global_meas_idxs + ) + ) + elif t.is_sweep_bit_target: + if self.sweep_bit_func is None: + raise ValueError( + f"Can't handle sweep bit target {t} in {op0} in circuit 0 " + "when sweep_bit_func is not provided." + ) + new_t = stim.target_sweep_bit(self.sweep_bit_func(0, t.value)) + else: + raise ValueError(f"Unrecognised GateTarget {t} in {op0} in circuit 0") + new_pair.append(new_t) + + target_pairs.append(tuple(new_pair)) + + # OP1 TARGETS + # this time, we have to use a list because we have to be able to index into it + op1_target_pairs = list(pairs(op1_targets)) + for pi, pair in enumerate(op1_target_pairs): + if pair in target_pairs: + if pair in op1_target_pairs[:pi]: + raise ValueError(f"Duplicate target pair {pair} in 2Q gate {op1} in circuit 1") + else: # it was in op0 + continue + if pair[::-1] in target_pairs: + raise ValueError( + f"Duplicate reversed target pair {pair}/{pair[::-1]}" + f" in 2Q gate {op0} in circuit 1" + ) + + new_pair = [] + for t in pair: + if t.is_qubit_target: + if t in touched_qubit_targets: + raise ValueError(f"Duplicate target {t} in 2Q gate {op1} in circuit 1") + else: + touched_qubit_targets.add(t) + new_t = t + elif t.is_measurement_record_target: + new_t = stim.target_rec( + self._global_lookback( + local_lookback=t.value, global_meas_idxs=self._c1_global_meas_idxs + ) + ) + elif t.is_sweep_bit_target: + if self.sweep_bit_func is None: + raise ValueError( + f"Can't handle sweep bit target {t} in {op1} in circuit 1 " + "when sweep_bit_func is not provided." + ) + new_t = stim.target_sweep_bit(self.sweep_bit_func(1, t.value)) + else: + raise ValueError(f"Unrecognised GateTarget {t} in {op1} in circuit 1") + new_pair.append(new_t) + + target_pairs.append(tuple(new_pair)) + + targets = list(itertools.chain.from_iterable(target_pairs)) + + self.circuit.append(name=op0.name, targets=targets, arg=op0.gate_args_copy()) + + def _handle_non_matching_operations( + self, op: stim.CircuitInstruction, global_meas_idxs: list[int] + ): + targets = [] + for t in op.targets_copy(): + if t.is_measurement_record_target: + targets.append( + stim.target_rec( + lookback_index=self._global_lookback( + local_lookback=t.value, global_meas_idxs=global_meas_idxs + ) + ) + ) + else: + raise ValueError(f"Unrecognised target {t} of non-matching op {op}") + self.circuit.append(name=op.name, targets=targets, arg=op.gate_args_copy()) diff --git a/glue/stimflow/src/stimflow/_chunk/_weave_test.py b/glue/stimflow/src/stimflow/_chunk/_weave_test.py new file mode 100644 index 00000000..9df923c0 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_weave_test.py @@ -0,0 +1,361 @@ +import pytest +import stim + +from stimflow._chunk._weave import StimCircuitLoom + + +def test_sq(): + # check targets are correctly de-duplicated + a = stim.Circuit( + """ + S 0 1 2 + """ + ) + b = stim.Circuit( + """ + S 3 2 4 1 5 + """ + ) + c = stim.Circuit( + """ + S 0 1 2 3 4 5 + """ + ) + assert StimCircuitLoom.weave(a, b) == c + + with pytest.raises(ValueError, match="Duplicate"): + a = stim.Circuit( + """ + S 0 0 + """ + ) + b = stim.Circuit( + """ + S 1 + """ + ) + StimCircuitLoom.weave(a, b) + + +def test_m(): + # check M targets are combined correctly, + # and that later references to them are remapped correctly + a = stim.Circuit( + """ + M 0 1 2 + DETECTOR rec[-1] + """ + ) + b = stim.Circuit( + """ + M 3 4 5 0 + DETECTOR rec[-1] + """ + ) + c = stim.Circuit( + """ + M 0 1 2 3 4 5 + DETECTOR rec[-4] + DETECTOR rec[-6] + """ + ) + assert StimCircuitLoom.weave(a, b) == c + + +def test_det_skipping(): + a = stim.Circuit( + """ + M 0 1 2 + DETECTOR rec[-3] + DETECTOR rec[-2] + DETECTOR rec[-1] + TICK + M 0 1 2 + DETECTOR rec[-1] + """ + ) + b = stim.Circuit( + """ + M + TICK + M + """ + ) # annoyingly we need the tick to prevent the Ms combining + c = stim.Circuit( + """ + M 0 1 2 + DETECTOR rec[-3] + DETECTOR rec[-2] + DETECTOR rec[-1] + TICK + M 0 1 2 + DETECTOR rec[-1] + """ + ) + assert StimCircuitLoom.weave(a, b) == c + + +def test_m_duplicates(): + # check M targets are combined correctly, + # and that later references to them are remapped correctly + a = stim.Circuit( + """ + M 0 1 + DETECTOR rec[-2] + DETECTOR rec[-1] + """ + ) + b = stim.Circuit( + """ + M 1 2 + DETECTOR rec[-2] + DETECTOR rec[-1] + """ + ) + c = stim.Circuit( + """ + M 0 1 2 + DETECTOR rec[-3] + DETECTOR rec[-2] + DETECTOR rec[-2] + DETECTOR rec[-1] + """ + ) + assert StimCircuitLoom.weave(a, b) == c + + with pytest.raises(ValueError, match="Duplicate"): + a = stim.Circuit( + """ + M 0 0 + """ + ) + b = stim.Circuit( + """ + M 1 + """ + ) + StimCircuitLoom.weave(a, b) + + with pytest.raises(ValueError, match="Duplicate"): + a = stim.Circuit( + """ + M 0 + """ + ) + b = stim.Circuit( + """ + M 1 1 + """ + ) + StimCircuitLoom.weave(a, b) + + +def test_sweep_bits(): + a = stim.Circuit( + """ + CX sweep[0] 0 sweep[1] 2 + """ + ) + b = stim.Circuit( + """ + CX sweep[0] 1 sweep[1] 3 + """ + ) + + sweep_bit_func = lambda circuit_idx, bit_idx: 10 * bit_idx + circuit_idx + c = stim.Circuit( + """ + CX sweep[00] 0 sweep[10] 2 sweep[01] 1 sweep[11] 3 + """ + ) + assert StimCircuitLoom.weave(a, b, sweep_bit_func) == c + + with pytest.raises(ValueError, match="sweep_bit_func is not provided"): + StimCircuitLoom.weave(a, b) + + +def test_2q(): + a = stim.Circuit( + """ + CX 0 1 2 3 + CZ 0 1 + CX sweep[0] 0 sweep[1] 2 + M 0 1 + CX rec[-2] 3 rec[-1] 1 + """ + ) + b = stim.Circuit( + """ + CX 0 1 4 5 + CZ 0 1 + CX sweep[0] 1 sweep[1] 3 + M 0 2 + CX rec[-2] 0 rec[-1] 2 + """ + ) + sweep_bit_func = lambda circuit_idx, bit_idx: 2 * bit_idx + circuit_idx + c = stim.Circuit( + """ + CX 0 1 2 3 4 5 + CZ 0 1 + CX sweep[0] 0 sweep[2] 2 sweep[1] 1 sweep[3] 3 + M 0 1 2 + CX rec[-3] 3 rec[-2] 1 rec[-3] 0 rec[-1] 2 + """ + ) + assert StimCircuitLoom.weave(a, b, sweep_bit_func) == c + + with pytest.raises(ValueError, match="Duplicate"): + a = stim.Circuit( + """ + CX 0 1 0 1 + """ + ) + b = stim.Circuit( + """ + CX 0 1 + """ + ) + StimCircuitLoom.weave(a, b) + + with pytest.raises(ValueError, match="Duplicate"): + a = stim.Circuit( + """ + CX 0 1 + """ + ) + b = stim.Circuit( + """ + CX 0 1 0 1 + """ + ) + StimCircuitLoom.weave(a, b) + + with pytest.raises(ValueError, match="Duplicate"): + a = stim.Circuit( + """ + CX 0 1 + """ + ) + b = stim.Circuit( + """ + CX 1 2 + """ + ) + StimCircuitLoom.weave(a, b) + + with pytest.raises(ValueError, match="Duplicate reversed"): + a = stim.Circuit( + """ + CZ 0 1 + """ + ) + b = stim.Circuit( + """ + CZ 1 0 + """ + ) + StimCircuitLoom.weave(a, b) + + +def test_small_rep_code(): + # 0 1 2 3 4 5 6 + a = stim.Circuit( + """ + R 1 3 + TICK + CX 0 1 2 3 + TICK + CX 2 1 4 3 + TICK + M 1 3 + DETECTOR rec[-1] + DETECTOR rec[-2] + """ + ) + b = stim.Circuit( + """ + R 3 5 + TICK + CX 2 3 4 5 + TICK + CX 4 3 6 5 + TICK + M 3 5 + DETECTOR rec[-1] + DETECTOR rec[-2] + """ + ) + c = stim.Circuit( + """ + R 1 3 5 + TICK + CX 0 1 2 3 4 5 + TICK + CX 2 1 4 3 6 5 + TICK + M 1 3 5 + DETECTOR rec[-2] + DETECTOR rec[-3] + DETECTOR rec[-1] + DETECTOR rec[-2] + """ + ) + assert StimCircuitLoom.weave(a, b) == c + + +def test_weaves_target_recs(): + # copied from rep code test + a = stim.Circuit( + """ + R 1 3 + TICK + CX 0 1 2 3 + TICK + CX 2 1 4 3 + TICK + M 1 3 + DETECTOR rec[-1] + DETECTOR rec[-2] + """ + ) + b = stim.Circuit( + """ + R 3 5 + TICK + CX 2 3 4 5 + TICK + CX 4 3 6 5 + TICK + M 3 5 + DETECTOR rec[-1] + DETECTOR rec[-2] + """ + ) + c = stim.Circuit( + """ + R 1 3 5 + TICK + CX 0 1 2 3 4 5 + TICK + CX 2 1 4 3 6 5 + TICK + M 1 3 5 + DETECTOR rec[-2] + DETECTOR rec[-3] + DETECTOR rec[-1] + DETECTOR rec[-2] + """ + ) + loom = StimCircuitLoom(a, b) + assert loom.circuit == c # should pass if above test passes + + assert loom.weaved_target_rec_from_c0(-2) == -3 + assert loom.weaved_target_rec_from_c0(-1) == -2 + assert loom.weaved_target_rec_from_c1(-2) == -2 + assert loom.weaved_target_rec_from_c1(-1) == -1 + + assert loom.weaved_target_rec_from_c0(0) == -3 + assert loom.weaved_target_rec_from_c0(1) == -2 + assert loom.weaved_target_rec_from_c1(0) == -2 + assert loom.weaved_target_rec_from_c1(1) == -1 diff --git a/glue/stimflow/src/stimflow/_core/__init__.py b/glue/stimflow/src/stimflow/_core/__init__.py new file mode 100644 index 00000000..389fe56e --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/__init__.py @@ -0,0 +1,22 @@ +from stimflow._core._circuit_util import ( + append_reindexed_content_to_circuit, + circuit_to_dem_target_measurement_records_map, + circuit_with_xz_flipped, + count_measurement_layers, + gate_counts_for_circuit, + gates_used_by_circuit, + stim_circuit_with_transformed_coords, + stim_circuit_with_transformed_moments, +) +from stimflow._core._complex_util import ( + complex_key, + min_max_complex, + sorted_complex, + xor_sorted, +) +from stimflow._core._flow import Flow +from stimflow._core._noise import NoiseModel, NoiseRule +from stimflow._core._pauli_map import PauliMap +from stimflow._core._str_html import str_html +from stimflow._core._str_svg import str_svg +from stimflow._core._tile import Tile diff --git a/glue/stimflow/src/stimflow/_core/_circuit_util.py b/glue/stimflow/src/stimflow/_core/_circuit_util.py new file mode 100644 index 00000000..fa7c84d3 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_circuit_util.py @@ -0,0 +1,415 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable +from typing import Literal + +import stim + + +def circuit_with_xz_flipped(circuit: stim.Circuit) -> stim.Circuit: + result = stim.Circuit() + for inst in circuit: + if isinstance(inst, stim.CircuitRepeatBlock): + result.append( + stim.CircuitRepeatBlock( + body=circuit_with_xz_flipped(inst.body_copy()), repeat_count=inst.repeat_count + ) + ) + else: + other = stim.gate_data(inst.name).hadamard_conjugated(unsigned=True).name + if other is None: + raise NotImplementedError(f"{inst=}") + result.append( + stim.CircuitInstruction(other, inst.targets_copy(), inst.gate_args_copy()) + ) + return result + + +def circuit_to_dem_target_measurement_records_map( + circuit: stim.Circuit, +) -> dict[stim.DemTarget, list[int]]: + result: dict[stim.DemTarget, list[int]] = {} + for k in range(circuit.num_observables): + result[stim.target_logical_observable_id(k)] = [] + num_d = 0 + num_m = 0 + for inst in circuit.flattened(): + if inst.name == "DETECTOR": + result[stim.target_relative_detector_id(num_d)] = [ + num_m + t.value for t in inst.targets_copy() + ] + num_d += 1 + elif inst.name == "OBSERVABLE_INCLUDE": + result[stim.target_logical_observable_id(int(inst.gate_args_copy()[0]))].extend( + num_m + t.value for t in inst.targets_copy() + ) + else: + c = stim.Circuit() + c.append(inst) + num_m += c.num_measurements + return result + + +def count_measurement_layers(circuit: stim.Circuit) -> int: + saw_measurement = False + result = 0 + for instruction in circuit: + if isinstance(instruction, stim.CircuitRepeatBlock): + result += count_measurement_layers(instruction.body_copy()) * instruction.repeat_count + elif isinstance(instruction, stim.CircuitInstruction): + saw_measurement |= stim.gate_data(instruction.name).produces_measurements + if instruction.name == "TICK": + result += saw_measurement + saw_measurement = False + else: + raise NotImplementedError(f"{instruction=}") + result += saw_measurement + return result + + +def gate_counts_for_circuit(circuit: stim.Circuit) -> collections.Counter[str]: + """Determines gates used by a circuit, disambiguating MPP/feedback cases. + + MPP instructions are expanded into what they actually measure, such as + "MXX" for MPP X1*X2 and "MXYZ" for MPP X4*Y5*Z7. + + Feedback instructions like `CX rec[-1] 0` become the gate "feedback". + + Sweep instructions like `CX sweep[2] 0` become the gate "sweep". + """ + ANNOTATION_OPS = { + "DETECTOR", + "OBSERVABLE_INCLUDE", + "QUBIT_COORDS", + "SHIFT_COORDS", + "TICK", + "MPAD", + } + + out: collections.Counter[str] = collections.Counter() + for instruction in circuit: + if isinstance(instruction, stim.CircuitRepeatBlock): + for gate_name, v in gate_counts_for_circuit(instruction.body_copy()).items(): + out[gate_name] += v * instruction.repeat_count + + elif instruction.name in ["CX", "CY", "CZ", "XCZ", "YCZ"]: + targets = instruction.targets_copy() + for k in range(0, len(targets), 2): + if ( + targets[k].is_measurement_record_target + or targets[k + 1].is_measurement_record_target + ): + out["feedback"] += 1 + elif targets[k].is_sweep_bit_target or targets[k + 1].is_sweep_bit_target: + out["sweep"] += 1 + else: + out[instruction.name] += 1 + + elif instruction.name == "MPP": + op = "M" + targets = instruction.targets_copy() + is_continuing = True + for t in targets: + if t.is_combiner: + is_continuing = True + continue + p = ( + "X" + if t.is_x_target + else "Y" if t.is_y_target else "Z" if t.is_z_target else "?" + ) + if is_continuing: + op += p + is_continuing = False + else: + if op == "MZ": + op = "M" + out[op] += 1 + op = "M" + p + if op: + if op == "MZ": + op = "M" + out[op] += 1 + + elif stim.gate_data(instruction.name).is_two_qubit_gate: + out[instruction.name] += len(instruction.targets_copy()) // 2 + elif ( + instruction.name in ANNOTATION_OPS + or instruction.name == "E" + or instruction.name == "ELSE_CORRELATED_ERROR" + ): + out[instruction.name] += 1 + else: + out[instruction.name] += len(instruction.targets_copy()) + + return out + + +def gates_used_by_circuit(circuit: stim.Circuit) -> set[str]: + """Determines gates used by a circuit, disambiguating MPP/feedback cases. + + MPP instructions are expanded into what they actually measure, such as + "MXX" for MPP X1*X2 and "MXYZ" for MPP X4*Y5*Z7. + + Feedback instructions like `CX rec[-1] 0` become the gate "feedback". + + Sweep instructions like `CX sweep[2] 0` become the gate "sweep". + """ + out = set() + for instruction in circuit: + if isinstance(instruction, stim.CircuitRepeatBlock): + out |= gates_used_by_circuit(instruction.body_copy()) + + elif instruction.name in ["CX", "CY", "CZ", "XCZ", "YCZ"]: + for a, b in instruction.target_groups(): + if a.is_measurement_record_target or b.is_measurement_record_target: + out.add("feedback") + elif a.is_sweep_bit_target or b.is_sweep_bit_target: + out.add("sweep") + else: + out.add(instruction.name) + + elif instruction.name == "MPP": + op = "M" + targets = instruction.targets_copy() + is_continuing = True + for t in targets: + if t.is_combiner: + is_continuing = True + continue + p = ( + "X" + if t.is_x_target + else "Y" if t.is_y_target else "Z" if t.is_z_target else "?" + ) + if is_continuing: + op += p + is_continuing = False + else: + if op == "MZ": + op = "M" + out.add(op) + op = "M" + p + if op: + if op == "MZ": + op = "M" + out.add(op) + + else: + out.add(instruction.name) + + return out + + +def stim_circuit_with_transformed_coords( + circuit: stim.Circuit, transform: Callable[[complex], complex] +) -> stim.Circuit: + """Returns an equivalent circuit, but with the qubit and detector position metadata modified. + The "position" is assumed to be the first two coordinates. These are mapped to the real and + imaginary values of a complex number which is then transformed. + + Note that `SHIFT_COORDS` instructions that modify the first two coordinates are not supported. + This is because supporting them requires flattening loops, or promising that the given + transformation is affine. + + Args: + circuit: The circuit with qubits to reposition. + transform: The transformation to apply to the positions. The positions are given one by one + to this method, as complex numbers. The method returns the new complex number for the + position. + + Returns: + The transformed circuit. + """ + result = stim.Circuit() + for instruction in circuit: + if isinstance(instruction, stim.CircuitInstruction): + if instruction.name == "QUBIT_COORDS" or instruction.name == "DETECTOR": + args = list(instruction.gate_args_copy()) + while len(args) < 2: + args.append(0) + c = transform(args[0] + args[1] * 1j) + args[0] = c.real + args[1] = c.imag + result.append(instruction.name, instruction.targets_copy(), args) + continue + if instruction.name == "SHIFT_COORDS": + args = instruction.gate_args_copy() + if any(args[:2]): + raise NotImplementedError(f"Shifting first two coords: {instruction=}") + + if isinstance(instruction, stim.CircuitRepeatBlock): + result.append( + stim.CircuitRepeatBlock( + repeat_count=instruction.repeat_count, + body=stim_circuit_with_transformed_coords(instruction.body_copy(), transform), + ) + ) + continue + + result.append(instruction) + return result + + +def stim_circuit_with_transformed_moments( + circuit: stim.Circuit, *, moment_func: Callable[[stim.Circuit], stim.Circuit] +) -> stim.Circuit: + """Applies a transformation to regions of a circuit separated by TICKs and blocks. + + For example, in this circuit: + + H 0 + X 0 + TICK + + H 1 + X 1 + REPEAT 100 { + H 2 + X 2 + } + H 3 + X 3 + + TICK + H 4 + X 4 + + `moment_func` would be called five times, each time with one of the H and X instruction pairs. + The result from the method would then be substituted into the circuit, replacing each of the H + and X instruction pairs. + + Args: + circuit: The circuit to return a transformed result of. + moment_func: The transformation to apply to regions of the circuit. Returns a new circuit + for the result. + + Returns: + A transformed circuit. + """ + + result = stim.Circuit() + current_moment = stim.Circuit() + + for instruction in circuit: + if isinstance(instruction, stim.CircuitRepeatBlock): + # Implicit tick at transition into REPEAT? + if current_moment: + result += moment_func(current_moment) + current_moment.clear() + + transformed_body = stim_circuit_with_transformed_moments( + instruction.body_copy(), moment_func=moment_func + ) + result.append( + stim.CircuitRepeatBlock( + repeat_count=instruction.repeat_count, body=transformed_body + ) + ) + elif isinstance(instruction, stim.CircuitInstruction) and instruction.name == "TICK": + # Explicit tick. Process even if empty. + result += moment_func(current_moment) + result.append("TICK") + current_moment.clear() + else: + current_moment.append(instruction) + + # Implicit tick at end of circuit? + if current_moment: + result += moment_func(current_moment) + + return result + + +def append_reindexed_content_to_circuit( + *, + out_circuit: stim.Circuit, + content: stim.Circuit, + qubit_i2i: dict[int, int], + obs_i2i: dict[int, int | Literal["discard"]], + rewrite_detector_time_coordinates: bool = False, +) -> None: + """Reindexes content and appends it to a circuit. + + Note that QUBIT_COORDS instructions are skipped. + + Args: + out_circuit: The output circuit. The circuit being edited. + content: The circuit to be appended to the output circuit. + qubit_i2i: A dictionary specifying how qubit indices are remapped. Indices outside the + map are not changed. + obs_i2i: A dictionary specifying how observable indices are remapped. Indices outside the + map are not changed. + rewrite_detector_time_coordinates: Defaults to False. When set to True, SHIFT_COORD and + DETECTOR instructions are automatically rewritten to track the passage of time without + using the same detector position twice at the same time. + """ + + def rewritten_targets(inst: stim.CircuitInstruction) -> list[stim.GateTarget]: + new_targets: list[int | stim.GateTarget] = [] + for t in inst.targets_copy(): + if t.is_qubit_target: + new_targets.append(qubit_i2i.get(t.value, t.value)) + elif t.is_x_target: + new_targets.append(stim.target_x(qubit_i2i.get(t.value, t.value))) + elif t.is_y_target: + new_targets.append(stim.target_y(qubit_i2i.get(t.value, t.value))) + elif t.is_z_target: + new_targets.append(stim.target_z(qubit_i2i.get(t.value, t.value))) + elif t.is_combiner: + new_targets.append(t) + elif t.is_measurement_record_target: + new_targets.append(t) + elif t.is_sweep_bit_target: + new_targets.append(t) + else: + raise NotImplementedError(f"{inst=}") + return new_targets + + det_offset_needed = 0 + for inst in content: + if inst.name == "REPEAT": + block = stim.Circuit() + append_reindexed_content_to_circuit( + content=inst.body_copy(), + qubit_i2i=qubit_i2i, + out_circuit=block, + rewrite_detector_time_coordinates=rewrite_detector_time_coordinates, + obs_i2i=obs_i2i, + ) + out_circuit.append( + stim.CircuitRepeatBlock(repeat_count=inst.repeat_count, body=block, tag=inst.tag) + ) + elif inst.name == "QUBIT_COORDS": + continue + elif inst.name == "SHIFT_COORDS": + if rewrite_detector_time_coordinates: + args = inst.gate_args_copy() + if len(args) > 2: + det_offset_needed -= args[2] + out_circuit.append("SHIFT_COORDS", [], [0, 0, args[2]], tag=inst.tag) + else: + out_circuit.append(inst) + elif inst.name == "OBSERVABLE_INCLUDE": + (obs_index,) = inst.gate_args_copy() + obs_index = int(round(obs_index)) + obs_index = obs_i2i.get(obs_index, obs_index) + if obs_index != "discard": + out_circuit.append( + "OBSERVABLE_INCLUDE", rewritten_targets(inst), obs_index, tag=inst.tag + ) + elif inst.name == "MPAD": + out_circuit.append(inst) + elif inst.name == "DETECTOR": + args = inst.gate_args_copy() + t = args[2] if len(args) > 2 else 0 + det_offset_needed = max(det_offset_needed, t + 1) + out_circuit.append(inst) + else: + out_circuit.append( + inst.name, rewritten_targets(inst), inst.gate_args_copy(), tag=inst.tag + ) + + if rewrite_detector_time_coordinates and det_offset_needed > 0: + out_circuit.append("SHIFT_COORDS", [], (0, 0, det_offset_needed)) diff --git a/glue/stimflow/src/stimflow/_core/_circuit_util_test.py b/glue/stimflow/src/stimflow/_core/_circuit_util_test.py new file mode 100644 index 00000000..b814bc6f --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_circuit_util_test.py @@ -0,0 +1,299 @@ +import stim + +import stimflow + + +def test_circuit_with_xz_flipped(): + assert ( + stimflow.circuit_with_xz_flipped( + stim.Circuit( + """ + CX 0 1 2 3 + TICK + H 0 + TICK + REPEAT 10 { + MXX 0 1 + } + """ + ) + ) + == stim.Circuit( + """ + XCZ 0 1 2 3 + TICK + H 0 + TICK + REPEAT 10 { + MZZ 0 1 + } + """ + ) + ) + + +def test_gates_used_by_circuit(): + assert ( + stimflow.gates_used_by_circuit( + stim.Circuit( + """ + H 0 + TICK + CX 0 1 + """ + ) + ) + == {"H", "TICK", "CX"} + ) + + assert ( + stimflow.gates_used_by_circuit( + stim.Circuit( + """ + S 0 + XCZ 0 1 + """ + ) + ) + == {"S", "XCZ"} + ) + + assert ( + stimflow.gates_used_by_circuit( + stim.Circuit( + """ + MPP X0*X1 Z2*Z3*Z4 Y0*Z1 + """ + ) + ) + == {"MXX", "MZZZ", "MYZ"} + ) + + assert ( + stimflow.gates_used_by_circuit( + stim.Circuit( + """ + CX rec[-1] 1 + """ + ) + ) + == {"feedback"} + ) + + assert ( + stimflow.gates_used_by_circuit( + stim.Circuit( + """ + CX sweep[1] 1 + """ + ) + ) + == {"sweep"} + ) + + assert ( + stimflow.gates_used_by_circuit( + stim.Circuit( + """ + CX rec[-1] 1 0 1 + """ + ) + ) + == {"feedback", "CX"} + ) + + +def test_gate_counts_for_circuit(): + assert ( + stimflow.gate_counts_for_circuit( + stim.Circuit( + """ + H 0 + TICK + CX 0 1 + """ + ) + ) + == {"H": 1, "TICK": 1, "CX": 1} + ) + + assert ( + stimflow.gate_counts_for_circuit( + stim.Circuit( + """ + S 0 + XCZ 0 1 + """ + ) + ) + == {"S": 1, "XCZ": 1} + ) + + assert ( + stimflow.gate_counts_for_circuit( + stim.Circuit( + """ + MPP X0*X1 Z2*Z3*Z4 Y0*Z1 + """ + ) + ) + == {"MXX": 1, "MZZZ": 1, "MYZ": 1} + ) + + assert ( + stimflow.gate_counts_for_circuit( + stim.Circuit( + """ + CX rec[-1] 1 + """ + ) + ) + == {"feedback": 1} + ) + + assert ( + stimflow.gate_counts_for_circuit( + stim.Circuit( + """ + CX sweep[1] 1 + """ + ) + ) + == {"sweep": 1} + ) + + assert ( + stimflow.gate_counts_for_circuit( + stim.Circuit( + """ + CX rec[-1] 1 0 1 + """ + ) + ) + == {"feedback": 1, "CX": 1} + ) + + assert ( + stimflow.gate_counts_for_circuit( + stim.Circuit( + """ + H 0 1 + REPEAT 100 { + S 0 1 2 + CX 0 1 2 3 + } + """ + ) + ) + == {"H": 2, "S": 300, "CX": 200} + ) + + +def test_count_measurement_layers(): + assert stimflow.count_measurement_layers(stim.Circuit()) == 0 + assert ( + stimflow.count_measurement_layers( + stim.Circuit( + """ + M 0 1 2 + """ + ) + ) + == 1 + ) + assert ( + stimflow.count_measurement_layers( + stim.Circuit( + """ + M 0 1 + MX 2 + MR 3 + """ + ) + ) + == 1 + ) + assert ( + stimflow.count_measurement_layers( + stim.Circuit( + """ + M 0 1 + MX 2 + TICK + MR 3 + """ + ) + ) + == 2 + ) + assert ( + stimflow.count_measurement_layers( + stim.Circuit( + """ + R 0 + CX 0 1 + TICK + M 0 + """ + ) + ) + == 1 + ) + assert ( + stimflow.count_measurement_layers( + stim.Circuit( + """ + R 0 + CX 0 1 + TICK + M 0 + DETECTOR rec[-1] + M 1 + DETECTOR rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + MPP X0*X1 + DETECTOR rec[-1] + """ + ) + ) + == 1 + ) + assert ( + stimflow.count_measurement_layers( + stim.Circuit.generated("repetition_code:memory", distance=3, rounds=4) + ) + == 4 + ) + assert ( + stimflow.count_measurement_layers( + stim.Circuit.generated("surface_code:rotated_memory_x", distance=3, rounds=1000) + ) + == 1000 + ) + + +def test_append_reindexed_content_to_circuit(): + circuit = stim.Circuit( + """ + H 5 1 + OBSERVABLE_INCLUDE(6) X7 rec[-3] + OBSERVABLE_INCLUDE(5) rec[-1] + OBSERVABLE_INCLUDE(1) Z7 rec[-5] + DETECTOR rec[-2] + """ + ) + new_circuit = stim.Circuit() + stimflow.append_reindexed_content_to_circuit( + content=circuit, + qubit_i2i={5: 15, 7: 27}, + obs_i2i={6: 2, 1: "discard"}, + out_circuit=new_circuit, + ) + assert new_circuit == stim.Circuit( + """ + H 15 1 + OBSERVABLE_INCLUDE(2) X27 rec[-3] + OBSERVABLE_INCLUDE(5) rec[-1] + DETECTOR rec[-2] + """ + ) diff --git a/glue/stimflow/src/stimflow/_core/_complex_util.py b/glue/stimflow/src/stimflow/_core/_complex_util.py new file mode 100644 index 00000000..ef887117 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_complex_util.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterable +from typing import Any, cast, TypeVar + + +def complex_key(c: complex) -> Any: + return c.real != int(c.real), c.real, c.imag + + +def sorted_complex(values: Iterable[complex]) -> list[complex]: + return sorted(values, key=complex_key) + + +def min_max_complex( + coords: Iterable[complex], *, default: complex | None = None +) -> tuple[complex, complex]: + """Computes the bounding box of a collection of complex numbers. + + Args: + coords: The complex numbers to place a bounding box around. + default: If no elements are included, the bounding box will cover this + single value when the collection of complex numbers is empty. If + this argument isn't set (or is set to None), an exception will be + raised instead when given an empty collection. + + Returns: + A pair of complex values (c_min, c_max) where c_min's real component + where c_min is the minimum corner of the bounding box and c_max is the + maximum corner of the bounding box. + """ + coords = list(coords) + if not coords and default is not None: + return default, default + real = [c.real for c in coords] + imag = [c.imag for c in coords] + min_r = min(real) + min_i = min(imag) + max_r = max(real) + max_i = max(imag) + return min_r + min_i * 1j, max_r + max_i * 1j + + +TItem = TypeVar("TItem") + + +def xor_sorted(vals: Iterable[TItem], *, key: Callable[[TItem], Any] | None = None) -> list[TItem]: + """Sorts items and then cancels pairs of equal items. + + An item will be in the result once if it appeared an odd number of times. + An item won't be in the result if it appeared an even number of times. + + Args: + vals: The items to sort. + key: An optional key function, mapping the items to keys that determine the + sorted order. Unequal items with the same key don't cancel. + """ + result = sorted(vals, key=cast(Any, key)) + n = len(result) + skipped = 0 + k = 0 + while k + 1 < n: + if result[k] == result[k + 1]: + skipped += 2 + k += 2 + else: + result[k - skipped] = result[k] + k += 1 + if k < n: + result[k - skipped] = result[k] + while skipped: + result.pop() + skipped -= 1 + return result diff --git a/glue/stimflow/src/stimflow/_core/_complex_util_test.py b/glue/stimflow/src/stimflow/_core/_complex_util_test.py new file mode 100644 index 00000000..f9f8489f --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_complex_util_test.py @@ -0,0 +1,32 @@ +import pytest + +import stimflow + + +def test_sorted_complex(): + assert stimflow.sorted_complex([1, 2j, 2, 1 + 2j]) == [2j, 1, 1 + 2j, 2] + + +def test_min_max_complex(): + with pytest.raises(ValueError): + stimflow.min_max_complex([]) + assert stimflow.min_max_complex([], default=0) == (0, 0) + assert stimflow.min_max_complex([], default=1 + 2j) == (1 + 2j, 1 + 2j) + assert stimflow.min_max_complex([1j], default=0) == (1j, 1j) + assert stimflow.min_max_complex([1j, 2]) == (0, 2 + 1j) + assert stimflow.min_max_complex([1j + 1, 2]) == (1, 2 + 1j) + + +def test_xor_sorted(): + assert stimflow.xor_sorted([]) == [] + assert stimflow.xor_sorted([2]) == [2] + assert stimflow.xor_sorted([2, 3]) == [2, 3] + assert stimflow.xor_sorted([3, 2]) == [2, 3] + assert stimflow.xor_sorted([2, 2]) == [] + assert stimflow.xor_sorted([2, 2, 2]) == [2] + assert stimflow.xor_sorted([2, 2, 2, 2]) == [] + assert stimflow.xor_sorted([2, 2, 3]) == [3] + assert stimflow.xor_sorted([3, 2, 2]) == [3] + assert stimflow.xor_sorted([2, 3, 2]) == [3] + assert stimflow.xor_sorted([2, 3, 3]) == [2] + assert stimflow.xor_sorted([2, 3, 5, 7, 11, 13, 5]) == [2, 3, 7, 11, 13] diff --git a/glue/stimflow/src/stimflow/_core/_flow.py b/glue/stimflow/src/stimflow/_core/_flow.py new file mode 100644 index 00000000..6faee9c9 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_flow.py @@ -0,0 +1,306 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterable, Mapping +from typing import Any, cast + +import stim + +from stimflow._core._complex_util import xor_sorted +from stimflow._core._pauli_map import PauliMap +from stimflow._core._tile import Tile + + +class _UNSPECIFIED_: + def __repr__(self): + return "" +_UNSPECIFIED: Any = _UNSPECIFIED_() + + +class Flow: + """A rule for how a stabilizer travels into, through, and/or out of a circuit.""" + + def __init__( + self, + *, + start: PauliMap | Tile | None = None, + end: PauliMap | Tile | None = None, + mids: Iterable[int] = (), + obs_key: Any = None, + center: complex | None = None, + flags: Iterable[Any] = frozenset(), + sign: bool | None = None, + ): + """Initializes a Flow. + + Args: + start: Defaults to None (empty). The Pauli product operator at the beginning of the + circuit (before *all* operations, including resets). + end: Defaults to None (empty). The Pauli product operator at the end of the + circuit (after *all* operations, including measurements). + mids: Defaults to empty. Indices of measurements that mediate the flow (that multiply + into it as it traverses the circuit). + center: Defaults to None (unspecified). Specifies a 2d coordinate to use in metadata + when the flow is completed into a detector. Incompatible with obs_key. + obs_key: Defaults to None (detector flow). Identifies that this is an observable flow + (instead of a detector flow) and gives a name that be used when linking chunks. + flags: Defaults to empty. Custom information about the flow, that can be used by code + operating on chunks for a variety of purposes. For example, this could identify the + "color" of the flow in a color code. + sign: Defaults to None (unsigned). The expected sign of the flow. + """ + if start == "auto": + raise ValueError(f"stimflow.Flow no longer supports {start=}. Use stimflow.FlowSemiAuto instead.") + if end == "auto": + raise ValueError(f"stimflow.Flow no longer supports {end=}. Use stimflow.FlowSemiAuto instead.") + if mids == "auto": + raise ValueError(f"stimflow.Flow no longer supports {mids=}. Use stimflow.FlowSemiAuto instead.") + if obs_key is None and center is None: + if isinstance(start, Tile) and start.measure_qubit is not None: + center = start.measure_qubit + if isinstance(end, Tile) and end.measure_qubit is not None: + center = end.measure_qubit + if obs_key is None and center is None: + qubits: list[complex] = [] + if isinstance(start, PauliMap): + qubits.extend(start.keys()) + if isinstance(end, PauliMap): + qubits.extend(end.keys()) + if isinstance(start, Tile): + qubits.extend(start.data_set) + if isinstance(end, Tile): + qubits.extend(end.data_set) + center = sum(qubits) / (len(qubits) or 1) + if isinstance(flags, str): + raise TypeError(f"{flags=} is a str instead of a set") + if obs_key is None and isinstance(start, PauliMap) and start.name is not None: + obs_key = start.name + if obs_key is None and isinstance(end, PauliMap) and end.name is not None: + obs_key = end.name + if isinstance(start, PauliMap) and start.name is not None: + assert obs_key == start.name + start = start.with_name(None) + if isinstance(end, PauliMap) and end.name is not None: + assert obs_key == end.name + end = end.with_name(None) + + if start is not None and not isinstance(start, (PauliMap, Tile)): + raise ValueError( + f"{start=} is not None and not isinstance(start, (stimflow.PauliMap, stimflow.Tile))" + ) + if end is not None and not isinstance(end, (PauliMap, Tile)): + raise ValueError( + f"{end=} is not None and not isinstance(end, (stimflow.PauliMap, stimflow.Tile))" + ) + if isinstance(start, Tile): + start = start.to_pauli_map() + elif start is None: + start = PauliMap() + if isinstance(end, Tile): + end = end.to_pauli_map() + elif end is None: + end = PauliMap() + self.start: PauliMap = start.with_name(obs_key) + self.end: PauliMap = end.with_name(obs_key) + self.measurement_indices: tuple[int, ...] = tuple(xor_sorted(mids)) + self.flags: frozenset[Any] = frozenset(flags) + self.center: complex | None = center + self.sign: bool | None = sign + + def to_stim_flow( + self, *, q2i: dict[complex, int], o2i: Mapping[Any, int | None] | None = None + ) -> stim.Flow: + out = self.end.to_stim_pauli_string(q2i) + if self.sign: + out.sign = -1 + included_observables: list[int] | None + if self.obs_key is None: + included_observables = None + elif o2i is None: + raise ValueError(f"{self.obs_key=} is not None but {o2i=}") + else: + v = o2i[self.obs_key] + if v is None: + included_observables = None + else: + included_observables = [v] + return stim.Flow( + input=self.start.to_stim_pauli_string(q2i), + output=out, + measurements=self.measurement_indices, + included_observables=included_observables, + ) + + @property + def obs_key(self) -> Any: + return self.start.name + + def with_edits( + self, + *, + start: PauliMap = _UNSPECIFIED, + end: PauliMap = _UNSPECIFIED, + measurement_indices: Iterable[int] = _UNSPECIFIED, + obs_key: Any = _UNSPECIFIED, + center: complex = _UNSPECIFIED, + flags: Iterable[str] = _UNSPECIFIED, + sign: Any = _UNSPECIFIED, + ) -> Flow: + return Flow( + start=self.start if start is _UNSPECIFIED else start, + end=self.end if end is _UNSPECIFIED else end, + mids=( + self.measurement_indices + if measurement_indices is _UNSPECIFIED + else cast(Any, measurement_indices) + ), + obs_key=self.obs_key if obs_key is _UNSPECIFIED else obs_key, + center=self.center if center is _UNSPECIFIED else center, + flags=self.flags if flags is _UNSPECIFIED else flags, + sign=self.sign if sign is _UNSPECIFIED else sign, + ) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Flow): + return NotImplemented + return ( + self.start == other.start + and self.end == other.end + and self.measurement_indices == other.measurement_indices + and self.obs_key == other.obs_key + and self.flags == other.flags + and self.center == other.center + and self.sign == other.sign + ) + + def __hash__(self) -> int: + return hash( + ( + self.start, + self.end, + self.measurement_indices, + self.obs_key, + self.flags, + self.center, + self.sign, + ) + ) + + def __str__(self) -> str: + q: Any + + start_terms = [] + for q, p in self.start.items(): + q = complex(q) + if q.real == 0: + q = "0+" + str(q) + q = str(q).replace("(", "").replace(")", "") + start_terms.append(f"{p}[{q}]") + + end_terms = [] + for q, p in self.end.items(): + q = complex(q) + if q.real == 0: + q = "0+" + str(q) + q = str(q).replace("(", "").replace(")", "") + end_terms.append(f"{p}[{q}]") + + for m in self.measurement_indices: + end_terms.append(f"rec[{m}]") + + if not start_terms: + start_terms.append("1") + if not end_terms: + end_terms.append("1") + + key = "" if self.obs_key is None else f" (obs={self.obs_key})" + result = f'{"*".join(start_terms)} -> {"*".join(end_terms)}{key}' + if self.sign is None: + pass + elif self.sign: + result = "-" + result + else: + result = "+" + result + if self.flags: + result += f" (flags={sorted(self.flags)})" + return result + + def __repr__(self): + return ( + f"stimflow.Flow(start={self.start!r}, " + f"end={self.end!r}, " + f"measurement_indices={self.measurement_indices!r}, " + f"flags={self.flags!r}, " + f"obs_key={self.obs_key!r}, " + f"center={self.center!r}, " + f"sign={self.sign!r}" + ) + + def with_xz_flipped(self) -> Flow: + return self.with_edits(start=self.start.with_xz_flipped(), end=self.end.with_xz_flipped()) + + def with_transformed_coords(self, transform: Callable[[complex], complex]) -> Flow: + return self.with_edits( + start=self.start.with_transformed_coords(transform), + end=self.end.with_transformed_coords(transform), + center=None if self.center is None else transform(self.center), + ) + + def fuse_with_next_flow(self, next_flow: Flow, *, next_flow_measure_offset: int) -> Flow: + if next_flow.start != self.end: + raise ValueError("other.start != self.end") + if next_flow.obs_key != self.obs_key: + raise ValueError("other.obs_key != self.obs_key") + if self.center is None: + new_center = next_flow.center + elif next_flow.center is None: + new_center = self.center + else: + new_center = (self.center + next_flow.center) / 2 + assert isinstance(self.measurement_indices, tuple) + assert isinstance(next_flow.measurement_indices, tuple) + return Flow( + start=self.start, + end=next_flow.end, + center=new_center, + mids=( + *(m + next_flow_measure_offset * (m < 0) for m in self.measurement_indices), + *(m + next_flow_measure_offset for m in next_flow.measurement_indices), + ), + obs_key=self.obs_key, + flags=self.flags | next_flow.flags, + sign=( + None if self.sign is None or next_flow.sign is None else self.sign ^ next_flow.sign + ), + ) + + def __mul__(self, other: Flow) -> Flow: + """Computes the product of two flows. + + The product of A -> B and C -> D is (A*C) -> (B*D). + """ + if self.obs_key != other.obs_key: + raise ValueError(f"{self.obs_key=} != {other.obs_key=}") + if (self.sign is None) != (other.sign is None): + raise ValueError(f"({self.sign=} is None) != ({other.sign=} is None)") + + new_start: PauliMap = self.start * other.start + new_end: PauliMap = self.end * other.end + new_center: complex | None + if self.center is not None and other.center is not None: + new_center = (self.center + other.center) / 2 + elif self.center is not None: + new_center = self.center + elif other.center is not None: + new_center = other.center + else: + new_center = None + + return Flow( + start=new_start, + end=new_end, + mids=xor_sorted(self.measurement_indices + other.measurement_indices), + obs_key=self.obs_key, + flags=self.flags | other.flags, + center=new_center, + sign=(None if self.sign is None else self.sign ^ other.sign), + ) diff --git a/glue/stimflow/src/stimflow/_core/_flow_test.py b/glue/stimflow/src/stimflow/_core/_flow_test.py new file mode 100644 index 00000000..2618da15 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_flow_test.py @@ -0,0 +1,7 @@ +import stimflow + + +def test_with_xz_flipped(): + assert stimflow.Flow(start=stimflow.PauliMap({1: "X", 2: "Z"}), center=0).with_xz_flipped() == stimflow.Flow( + start=stimflow.PauliMap({1: "Z", 2: "X"}), center=0 + ) diff --git a/glue/stimflow/src/stimflow/_core/_noise.py b/glue/stimflow/src/stimflow/_core/_noise.py new file mode 100644 index 00000000..5e47cb37 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_noise.py @@ -0,0 +1,662 @@ +from __future__ import annotations + +import collections +from collections.abc import Iterable, Iterator, Set +from typing import Any + +import stim + +ANNOTATION_OPS = {"DETECTOR", "OBSERVABLE_INCLUDE", "QUBIT_COORDS", "SHIFT_COORDS", "TICK", "MPAD"} +OP_TO_MEASURE_BASES = { + "M": "Z", + "MR": "Z", + "MX": "X", + "MY": "Y", + "MZ": "Z", + "MRX": "X", + "MRY": "Y", + "MRZ": "Z", + "MXX": "XX", + "MYY": "YY", + "MZZ": "ZZ", + "MPP": "*", +} + + +class NoiseRule: + """Describes how to add noise to an operation.""" + + def __init__( + self, + *, + before: dict[str, float | tuple[float, ...]] | None = None, + after: dict[str, float | tuple[float, ...]] | None = None, + flip_result: float = 0, + ): + """ + + Args: + after: A dictionary mapping noise rule names to their probability argument. + For example, {"DEPOLARIZE2": 0.01, "X_ERROR": 0.02} will add two qubit + depolarization with parameter 0.01 and also add 2% bit flip noise. These + noise channels occur after all other operations in the moment and are applied + to the same targets as the relevant operation. + flip_result: The probability that a measurement result should be reported incorrectly. + Only valid when applied to operations that produce measurement results. + """ + if after is None: + after = {} + if before is None: + before = {} + if not (0 <= flip_result <= 1): + raise ValueError(f"not (0 <= {flip_result=} <= 1)") + for k, p_args in [*after.items(), *before.items()]: + gate_data = stim.gate_data(k) + if gate_data.produces_measurements or not gate_data.is_noisy_gate: + raise ValueError(f"not a pure noise channel: {k} from {after=}") + if gate_data.num_parens_arguments_range == range(1, 2): + if not isinstance(p_args, (int, float)) or not (0 <= p_args <= 1): + raise ValueError(f"not a probability: {p_args!r}") + else: + if not isinstance(p_args, (list, tuple)) or not (0 <= sum(p_args) <= 1): + raise ValueError(f"not a tuple of disjoint probabilities: {p_args!r}") + if len(p_args) not in gate_data.num_parens_arguments_range: + raise ValueError(f"Wrong number of arguments {p_args!r} for gate {k!r}") + self.before: dict[str, float | tuple[float, ...]] = before + self.after: dict[str, float | tuple[float, ...]] = after + self.flip_result: float = flip_result + + def append_noisy_version_of( + self, + *, + split_op: stim.CircuitInstruction, + out_during_moment: stim.Circuit, + before_moments: collections.defaultdict[Any, stim.Circuit], + after_moments: collections.defaultdict[Any, stim.Circuit], + immune_qubit_indices: Set[int], + ) -> None: + targets = split_op.targets_copy() + if immune_qubit_indices and any( + (t.is_qubit_target or t.is_x_target or t.is_y_target or t.is_z_target) + and t.value in immune_qubit_indices + for t in targets + ): + out_during_moment.append(split_op) + return + + args = split_op.gate_args_copy() + if self.flip_result: + gate_data = stim.gate_data(split_op.name) + assert gate_data.produces_measurements + assert gate_data.is_noisy_gate + assert gate_data.num_parens_arguments_range == range(0, 2) + assert len(args) == 0 + args = [self.flip_result] + + out_during_moment.append(split_op.name, targets, args, tag=split_op.tag) + raw_targets = [t.value for t in targets if not t.is_combiner] + for op_name, arg in self.before.items(): + before_moments[(op_name, arg)].append(op_name, raw_targets, arg) + for op_name, arg in self.after.items(): + after_moments[(op_name, arg)].append(op_name, raw_targets, arg) + + +class NoiseModel: + """Converts circuits into noisy circuits according to rules.""" + + def __init__( + self, + idle_depolarization: float = 0, + tick_noise: NoiseRule | None = None, + additional_depolarization_waiting_for_m_or_r: float = 0, + gate_rules: dict[str, NoiseRule] | None = None, + measure_rules: dict[str, NoiseRule] | None = None, + any_measurement_rule: NoiseRule | None = None, + any_clifford_1q_rule: NoiseRule | float | None = None, + any_clifford_2q_rule: NoiseRule | float | None = None, + allow_multiple_uses_of_a_qubit_in_one_tick: bool = False, + ): + if isinstance(any_clifford_1q_rule, float): + any_clifford_1q_rule = NoiseRule(after={"DEPOLARIZE1": any_clifford_1q_rule}) + if isinstance(any_clifford_2q_rule, float): + any_clifford_2q_rule = NoiseRule(after={"DEPOLARIZE2": any_clifford_2q_rule}) + self.idle_depolarization = idle_depolarization + self.tick_noise = tick_noise + self.additional_depolarization_waiting_for_m_or_r = ( + additional_depolarization_waiting_for_m_or_r + ) + self.gate_rules = {} if gate_rules is None else gate_rules + self.measure_rules = measure_rules + self.any_measurement_rule = any_measurement_rule + self.any_clifford_1q_rule = any_clifford_1q_rule + self.any_clifford_2q_rule = any_clifford_2q_rule + self.allow_multiple_uses_of_a_qubit_in_one_tick = allow_multiple_uses_of_a_qubit_in_one_tick + assert self.tick_noise is None or not self.tick_noise.flip_result + + @staticmethod + def si1000(p: float) -> NoiseModel: + """Superconducting inspired noise. + + As defined in "A Fault-Tolerant Honeycomb Memory" https://arxiv.org/abs/2108.10457 + + Small tweak when measurements aren't immediately followed by a reset: the measurement result + is probabilistically flipped instead of the input qubit. The input qubit is depolarized + after the measurement. + """ + return NoiseModel( + idle_depolarization=p / 10, + additional_depolarization_waiting_for_m_or_r=2 * p, + any_clifford_1q_rule=NoiseRule(after={"DEPOLARIZE1": p / 10}), + any_clifford_2q_rule=NoiseRule(after={"DEPOLARIZE2": p}), + measure_rules={ + "Z": NoiseRule(after={"DEPOLARIZE1": p}, flip_result=p * 5), + "ZZ": NoiseRule(after={"DEPOLARIZE2": p}, flip_result=p * 5), + }, + gate_rules={"R": NoiseRule(after={"X_ERROR": p * 2})}, + ) + + @staticmethod + def uniform_depolarizing(p: float, *, single_qubit_only: bool = False) -> NoiseModel: + """Near-standard circuit depolarizing noise. + + Everything has the same parameter p. + Single qubit clifford gates get single qubit depolarization. + Two qubit clifford gates get single qubit depolarization. + Dissipative gates have their result probabilistically bit flipped (or phase flipped if + appropriate). + + Non-demolition measurement is treated a bit unusually in that it is the result that is + flipped instead of the input qubit. The input qubit is depolarized. + """ + dep2 = "DEPOLARIZE1" if single_qubit_only else "DEPOLARIZE2" + return NoiseModel( + idle_depolarization=p, + any_clifford_1q_rule=NoiseRule(after={"DEPOLARIZE1": p}), + any_clifford_2q_rule=NoiseRule(after={dep2: p}), + measure_rules={ + "X": NoiseRule(after={"DEPOLARIZE1": p}, flip_result=p), + "Y": NoiseRule(after={"DEPOLARIZE1": p}, flip_result=p), + "Z": NoiseRule(after={"DEPOLARIZE1": p}, flip_result=p), + "XX": NoiseRule(after={dep2: p}, flip_result=p), + "XY": NoiseRule(after={dep2: p}, flip_result=p), + "XZ": NoiseRule(after={dep2: p}, flip_result=p), + "YX": NoiseRule(after={dep2: p}, flip_result=p), + "YY": NoiseRule(after={dep2: p}, flip_result=p), + "YZ": NoiseRule(after={dep2: p}, flip_result=p), + "ZX": NoiseRule(after={dep2: p}, flip_result=p), + "ZY": NoiseRule(after={dep2: p}, flip_result=p), + "ZZ": NoiseRule(after={dep2: p}, flip_result=p), + }, + gate_rules={ + "RX": NoiseRule(after={"Z_ERROR": p}), + "RY": NoiseRule(after={"X_ERROR": p}), + "R": NoiseRule(after={"X_ERROR": p}), + }, + ) + + def _noise_rule_for_split_operation( + self, *, split_op: stim.CircuitInstruction + ) -> NoiseRule | None: + if occurs_in_classical_control_system(split_op): + return None + + rule = self.gate_rules.get(split_op.name) + if rule is not None: + return rule + + gate_data = stim.gate_data(split_op.name) + + if ( + self.any_clifford_1q_rule is not None + and gate_data.is_unitary + and gate_data.is_single_qubit_gate + ): + return self.any_clifford_1q_rule + if ( + self.any_clifford_2q_rule is not None + and gate_data.is_unitary + and gate_data.is_two_qubit_gate + ): + return self.any_clifford_2q_rule + if self.measure_rules is not None: + rule = self.measure_rules.get(_measure_basis(split_op=split_op)) + if rule is not None: + return rule + if self.any_measurement_rule is not None and gate_data.produces_measurements: + return self.any_measurement_rule + if gate_data.is_reset and gate_data.produces_measurements: + m_name, r_name = {"MRX": ("MX", "RX"), "MRY": ("MY", "RY"), "MR": ("M", "R")}[ + gate_data.name + ] + r_noise = self._noise_rule_for_split_operation( + split_op=stim.CircuitInstruction(r_name, split_op.targets_copy(), tag=split_op.tag) + ) + m_noise = self._noise_rule_for_split_operation( + split_op=stim.CircuitInstruction(m_name, split_op.targets_copy(), tag=split_op.tag) + ) + return NoiseRule( + before=r_noise.before if r_noise is not None else {}, + after=r_noise.after if r_noise is not None else {}, + flip_result=m_noise.flip_result if m_noise is not None else 0, + ) + + raise ValueError(f"No noise (or lack of noise) specified for '{split_op}'.") + + def _append_idle_error( + self, + *, + moment_split_ops: list[stim.CircuitInstruction], + out: stim.Circuit, + system_qubit_indices: Set[int], + immune_qubit_indices: Set[int], + ) -> None: + collapse_qubits: list[int] = [] + clifford_qubits: list[int] = [] + pauli_qubits: list[int] = [] + for split_op in moment_split_ops: + if occurs_in_classical_control_system(split_op): + continue + gate_data = stim.gate_data(split_op.name) + qubits_out: list[int] + if gate_data.is_reset or gate_data.produces_measurements: + qubits_out = collapse_qubits + elif split_op.name in "IXYZ": + qubits_out = pauli_qubits + elif gate_data.is_unitary: + qubits_out = clifford_qubits + else: + raise NotImplementedError(f"{split_op=}") + for target in split_op.targets_copy(): + if not target.is_combiner: + qubits_out.append(target.value) + + # Safety check for operation collisions. + usage_counts = collections.Counter(collapse_qubits + clifford_qubits) + for pauli_qubit in pauli_qubits: + if usage_counts[pauli_qubit] == 0: + usage_counts[pauli_qubit] += 1 + qubits_used_multiple_times = {q for q, c in usage_counts.items() if c != 1} + if qubits_used_multiple_times and not self.allow_multiple_uses_of_a_qubit_in_one_tick: + moment = stim.Circuit() + for op in moment_split_ops: + moment.append(op) + raise ValueError( + f"Qubits were operated on multiple times without a TICK in between:\n" + f"multiple uses: {sorted(qubits_used_multiple_times)}\n" + f"moment:\n" + f"{moment}" + ) + + collapse_qubits_set = set(collapse_qubits) + clifford_qubits_set = set(clifford_qubits + pauli_qubits) + idle = sorted( + system_qubit_indices - collapse_qubits_set - clifford_qubits_set - immune_qubit_indices + ) + if idle and self.idle_depolarization: + out.append("DEPOLARIZE1", idle, self.idle_depolarization) + + waiting_for_mr = sorted(system_qubit_indices - collapse_qubits_set - immune_qubit_indices) + if ( + collapse_qubits_set + and waiting_for_mr + and self.additional_depolarization_waiting_for_m_or_r + ): + out.append("DEPOLARIZE1", idle, self.additional_depolarization_waiting_for_m_or_r) + + if self.tick_noise is not None: + for k, p in self.tick_noise.before.items(): + out.append(k, system_qubit_indices - immune_qubit_indices, p) + for k, p in self.tick_noise.after.items(): + out.append(k, system_qubit_indices - immune_qubit_indices, p) + + def _append_noisy_moment( + self, + *, + moment_split_ops: list[stim.CircuitInstruction], + out: stim.Circuit, + system_qubits_indices: Set[int], + immune_qubit_indices: Set[int], + ) -> None: + skip_pauli_targets: set[int] = set() + for split_op in moment_split_ops: + gate_data = stim.gate_data(split_op.name) + if ( + gate_data.is_unitary + and gate_data.is_single_qubit_gate + and not split_op.name in "IXYZ" + ): + skip_pauli_targets.update(t.qubit_value for t in split_op.targets_copy()) + + before: collections.defaultdict[Any, stim.Circuit] = collections.defaultdict(stim.Circuit) + after: collections.defaultdict[Any, stim.Circuit] = collections.defaultdict(stim.Circuit) + grow = stim.Circuit() + for split_op in moment_split_ops: + rule = self._noise_rule_for_split_operation(split_op=split_op) + if rule is None: + grow.append(split_op) + elif split_op.name in "IXYZ": + new_targets = [] + skipped_targets = [] + for t in split_op.targets_copy(): + if t.qubit_value in skip_pauli_targets: + skipped_targets.append(t) + else: + new_targets.append(t) + skip_pauli_targets.add(t.qubit_value) + if skipped_targets: + grow.append( + stim.CircuitInstruction( + split_op.name, skipped_targets, split_op.gate_args_copy(), tag=split_op.tag, + ) + ) + if new_targets: + rule.append_noisy_version_of( + split_op=stim.CircuitInstruction( + split_op.name, new_targets, split_op.gate_args_copy(), tag=split_op.tag, + ), + out_during_moment=grow, + before_moments=before, + after_moments=after, + immune_qubit_indices=immune_qubit_indices, + ) + else: + rule.append_noisy_version_of( + split_op=split_op, + out_during_moment=grow, + before_moments=before, + after_moments=after, + immune_qubit_indices=immune_qubit_indices, + ) + for k in sorted(before.keys()): + out += before[k] + out += grow + for k in sorted(after.keys()): + out += after[k] + + self._append_idle_error( + moment_split_ops=moment_split_ops, + out=out, + system_qubit_indices=system_qubits_indices, + immune_qubit_indices=immune_qubit_indices, + ) + + def noisy_circuit_skipping_mpp_boundaries( + self, + circuit: stim.Circuit, + *, + immune_qubit_indices: Set[int] | None = None, + immune_qubit_coords: Iterable[complex | float | int | Iterable[float | int]] | None = None, + ) -> stim.Circuit: + """Adds noise to the circuit except for MPP operations at the start/end. + + Divides the circuit into three parts: mpp_start, body, mpp_end. The mpp + sections grow from the ends of the circuit until they hit an instruction + that's not an annotation or an MPP. Then body is the remaining circuit + between the two ends. Noise is added to the body, and then the pieces + are reassembled. + """ + allowed = {"TICK", "OBSERVABLE_INCLUDE", "DETECTOR", "MPP", "QUBIT_COORDS", "SHIFT_COORDS"} + start = 0 + end = len(circuit) + while start < len(circuit) and circuit[start].name in allowed: + start += 1 + while end > 0 and circuit[end - 1].name in allowed: + end -= 1 + if end <= start: + raise ValueError("end <= start") + noisy = self.noisy_circuit( + circuit[start:end], + immune_qubit_indices=_immune_indices( + circuit, immune_qubit_indices, immune_qubit_coords + ), + ) + return circuit[:start] + noisy + circuit[end:] + + def noisy_circuit( + self, + circuit: stim.Circuit, + *, + system_qubit_indices: set[int] | None = None, + immune_qubit_indices: Iterable[int] | None = None, + immune_qubit_coords: Iterable[complex | float | int | Iterable[float | int]] | None = None, + ) -> stim.Circuit: + """Returns a noisy version of the given circuit, by applying the receiving noise model. + + Args: + circuit: The circuit to layer noise over. + system_qubit_indices: All qubits used by the circuit. These are the qubits eligible for + idling noise. + immune_qubit_indices: Qubits to not apply noise to, even if they are operated on. + immune_qubit_coords: Qubit coordinates to not apply noise to, even if they are operated + on. + + Returns: + The noisy version of the circuit. + """ + if system_qubit_indices is None: + system_qubit_indices = set(range(circuit.num_qubits)) + immune_qubit_indices = _immune_indices(circuit, immune_qubit_indices, immune_qubit_coords) + + result = stim.Circuit() + + first = True + for moment_split_ops in _iter_split_op_moments( + circuit, immune_qubit_indices=immune_qubit_indices + ): + if first: + first = False + elif result and isinstance(result[-1], stim.CircuitRepeatBlock): + pass + else: + result.append("TICK") + if isinstance(moment_split_ops, stim.CircuitRepeatBlock): + noisy_body = self.noisy_circuit( + moment_split_ops.body_copy(), + system_qubit_indices=system_qubit_indices, + immune_qubit_indices=immune_qubit_indices, + ) + noisy_body.append("TICK") + result.append( + stim.CircuitRepeatBlock( + repeat_count=moment_split_ops.repeat_count, body=noisy_body + ) + ) + else: + self._append_noisy_moment( + moment_split_ops=moment_split_ops, + out=result, + system_qubits_indices=system_qubit_indices, + immune_qubit_indices=immune_qubit_indices, + ) + + return result + + +def occurs_in_classical_control_system(op: stim.CircuitInstruction) -> bool: + """Determines if an operation is an annotation or a classical control system update.""" + if op.tag == 'noiseless-virtual': + return True + if op.name in ANNOTATION_OPS: + return True + + gate_data = stim.gate_data(op.name) + if gate_data.is_unitary and gate_data.is_two_qubit_gate: + targets = op.targets_copy() + for k in range(0, len(targets), 2): + a = targets[k] + b = targets[k + 1] + classical_0 = a.is_measurement_record_target or a.is_sweep_bit_target + classical_1 = b.is_measurement_record_target or b.is_sweep_bit_target + if not (classical_0 or classical_1): + return False + return True + return False + + +def _split_targets_if_needed( + op: stim.CircuitInstruction, immune_qubit_indices: Set[int] +) -> Iterator[stim.CircuitInstruction]: + """Splits operations into pieces as needed (e.g. MPP into each product, classical control away + from quantum ops).""" + gate_data = stim.gate_data(op.name) + if gate_data.is_unitary and gate_data.is_two_qubit_gate: + yield from _split_targets_if_needed_clifford_2q(op, immune_qubit_indices) + elif op.name == "MPP": + yield from _split_targets_if_needed_m_basis(op) + elif op.name in ANNOTATION_OPS: + yield op + elif gate_data.is_noisy_gate and not gate_data.produces_measurements: + yield op + elif gate_data.is_single_qubit_gate: + yield from _split_out_immune_targets_assuming_single_qubit_gate(op, immune_qubit_indices) + elif gate_data.is_two_qubit_gate: + yield from _split_out_immune_targets_assuming_two_qubit_gate(op, immune_qubit_indices) + else: + raise NotImplementedError(f"{op=}") + + +def _split_out_immune_targets_assuming_single_qubit_gate( + op: stim.CircuitInstruction, immune_qubit_indices: Set[int] +) -> Iterator[stim.CircuitInstruction]: + if immune_qubit_indices: + args = op.gate_args_copy() + for t in op.targets_copy(): + yield stim.CircuitInstruction(op.name, [t], args, tag=op.tag) + else: + yield op + + +def _split_out_immune_targets_assuming_two_qubit_gate( + op: stim.CircuitInstruction, immune_qubit_indices: Set[int] +) -> Iterator[stim.CircuitInstruction]: + if immune_qubit_indices: + args = op.gate_args_copy() + targets = op.targets_copy() + for k in range(len(targets)): + t1 = targets[k] + t2 = targets[k + 1] + yield stim.CircuitInstruction(op.name, [t1, t2], args, tag=op.tag) + else: + yield op + + +def _split_targets_if_needed_clifford_2q( + op: stim.CircuitInstruction, immune_qubit_indices: Set[int] +) -> Iterator[stim.CircuitInstruction]: + """Splits classical control system operations away from things actually happening on the quantum + computer.""" + gate_data = stim.gate_data(op.name) + assert gate_data.is_unitary and gate_data.is_two_qubit_gate + targets = op.targets_copy() + if immune_qubit_indices or any(t.is_measurement_record_target for t in targets): + args = op.gate_args_copy() + for k in range(0, len(targets), 2): + yield stim.CircuitInstruction(op.name, targets[k : k + 2], args, tag=op.tag) + else: + yield op + + +def _split_targets_if_needed_m_basis( + op: stim.CircuitInstruction, +) -> Iterator[stim.CircuitInstruction]: + """Splits an MPP operation into one operation for each Pauli product it measures.""" + targets = op.targets_copy() + args = op.gate_args_copy() + k = 0 + start = k + while k < len(targets): + if k + 1 == len(targets) or not targets[k + 1].is_combiner: + yield stim.CircuitInstruction(op.name, targets[start : k + 1], args, tag=op.tag) + k += 1 + start = k + else: + k += 2 + assert k == len(targets) + + +def _iter_split_op_moments( + circuit: stim.Circuit, *, immune_qubit_indices: Set[int] +) -> Iterator[stim.CircuitRepeatBlock | list[stim.CircuitInstruction]]: + """Splits a circuit into moments and some operations into pieces. + + Classical control system operations like CX rec[-1] 0 are split apart from quantum operations + like CX 1 0. + + MPP operations are split into one operation per Pauli product. + + Yields: + Lists of operations corresponding to one moment in the circuit, with any problematic + operations like MPPs split into pieces. + + (A moment is the time between two TICKs.) + """ + cur_moment: list[stim.CircuitInstruction] = [] + + for op in circuit: + if op.tag == 'noiseless-virtual': + cur_moment.append(op) + elif isinstance(op, stim.CircuitRepeatBlock): + if cur_moment: + yield cur_moment + cur_moment = [] + yield op + elif isinstance(op, stim.CircuitInstruction): + if op.name == "TICK": + yield cur_moment + cur_moment = [] + else: + cur_moment.extend( + _split_targets_if_needed(op, immune_qubit_indices=immune_qubit_indices) + ) + if cur_moment: + yield cur_moment + + +def _measure_basis(*, split_op: stim.CircuitInstruction) -> str | None: + """Converts an operation into a string describing the Pauli product basis it measures. + + Returns: + None: This is not a measurement (or not *just* a measurement). + str: Pauli product string that the operation measures (e.g. "XX" or "Y"). + """ + result = OP_TO_MEASURE_BASES.get(split_op.name) + if result == "*": + result = "" + targets = split_op.targets_copy() + for k in range(0, len(targets), 2): + t = targets[k] + if t.is_x_target: + result += "X" + elif t.is_y_target: + result += "Y" + elif t.is_z_target: + result += "Z" + else: + raise NotImplementedError(f"{targets=}") + return result + + +def _immune_indices( + circuit: stim.Circuit, + immune_qubit_indices: Iterable[int] | None = None, + immune_qubit_coords: Iterable[complex | float | int | Iterable[float | int]] | None = None, +) -> frozenset[int]: + result: set[int] = set() + if immune_qubit_indices is not None: + result.update(immune_qubit_indices) + if immune_qubit_coords is not None: + # Canonicalize the immune coordinates. + immune_qubit_coords = frozenset(immune_qubit_coords) + if immune_qubit_coords: + immune_tuples: set[tuple[float, ...]] = set() + for c in immune_qubit_coords: + if isinstance(c, (int, float, complex)): + immune_tuples.add((c.real, c.imag)) + else: + immune_tuples.add(tuple(c)) + + # Convert to indices. + for k, coord in circuit.get_final_qubit_coordinates().items(): + if tuple(coord) in immune_tuples: + result.add(k) + return frozenset(result) diff --git a/glue/stimflow/src/stimflow/_core/_noise_test.py b/glue/stimflow/src/stimflow/_core/_noise_test.py new file mode 100644 index 00000000..4c0159c9 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_noise_test.py @@ -0,0 +1,368 @@ +import stim + +import stimflow +from stimflow._core._noise import ( + _iter_split_op_moments, + _measure_basis, + NoiseModel, + occurs_in_classical_control_system, +) + + +def test_measure_basis(): + f = lambda e: _measure_basis(split_op=stim.Circuit(e)[0]) + assert f("H") is None + assert f("H 0") is None + assert f("R 0 1 2") is None + + assert f("MX") == "X" + assert f("MX(0.01) 1") == "X" + assert f("MY 0 1") == "Y" + assert f("MZ 0 1 2") == "Z" + assert f("M 0 1 2") == "Z" + + assert f("MRX") == "X" + + assert f("MPP X5") == "X" + assert f("MPP X0*X2") == "XX" + assert f("MPP Y0*Z2*X3") == "YZX" + + +def test_iter_split_op_moments(): + assert ( + list( + _iter_split_op_moments( + stim.Circuit( + """ + """ + ), + immune_qubit_indices=set(), + ) + ) + == [] + ) + + assert ( + list( + _iter_split_op_moments( + stim.Circuit( + """ + H 0 + """ + ), + immune_qubit_indices=set(), + ) + ) + == [[stim.CircuitInstruction("H", [0])]] + ) + + assert ( + list( + _iter_split_op_moments( + stim.Circuit( + """ + H 0 + TICK + """ + ), + immune_qubit_indices=set(), + ) + ) + == [[stim.CircuitInstruction("H", [0])]] + ) + + assert ( + list( + _iter_split_op_moments( + stim.Circuit( + """ + H 0 1 + TICK + """ + ), + immune_qubit_indices=set(), + ) + ) + == [[stim.CircuitInstruction("H", [0, 1])]] + ) + + assert ( + list( + _iter_split_op_moments( + stim.Circuit( + """ + H 0 1 + TICK + """ + ), + immune_qubit_indices={3}, + ) + ) + == [[stim.CircuitInstruction("H", [0]), stim.CircuitInstruction("H", [1])]] + ) + + assert ( + list( + _iter_split_op_moments( + stim.Circuit( + """ + H 0 + TICK + H 1 + """ + ), + immune_qubit_indices=set(), + ) + ) + == [[stim.CircuitInstruction("H", [0])], [stim.CircuitInstruction("H", [1])]] + ) + + assert ( + list( + _iter_split_op_moments( + stim.Circuit( + """ + CX rec[-1] 0 1 2 3 4 + MPP X5*X6 Y5 + CX 8 9 10 11 + TICK + H 0 + """ + ), + immune_qubit_indices=set(), + ) + ) + == [ + [ + stim.CircuitInstruction("CX", [stim.target_rec(-1), 0]), + stim.CircuitInstruction("CX", [1, 2]), + stim.CircuitInstruction("CX", [3, 4]), + stim.CircuitInstruction( + "MPP", [stim.target_x(5), stim.target_combiner(), stim.target_x(6)] + ), + stim.CircuitInstruction("MPP", [stim.target_y(5)]), + stim.CircuitInstruction("CX", [8, 9, 10, 11]), + ], + [stim.CircuitInstruction("H", [0])], + ] + ) + + +def test_occurs_in_classical_control_system(): + assert not occurs_in_classical_control_system(op=stim.CircuitInstruction("H", [0])) + assert not occurs_in_classical_control_system(op=stim.CircuitInstruction("CX", [0, 1, 2, 3])) + assert not occurs_in_classical_control_system(op=stim.CircuitInstruction("M", [0, 1, 2, 3])) + + assert occurs_in_classical_control_system( + op=stim.CircuitInstruction("CX", [stim.target_rec(-1), 0]) + ) + assert occurs_in_classical_control_system( + op=stim.CircuitInstruction("DETECTOR", [stim.target_rec(-1)]) + ) + assert occurs_in_classical_control_system(op=stim.CircuitInstruction("TICK", [])) + assert occurs_in_classical_control_system(op=stim.CircuitInstruction("SHIFT_COORDS", [])) + + +def test_si_1000(): + model = NoiseModel.si1000(1e-3) + assert ( + model.noisy_circuit( + stim.Circuit( + """ + R 0 1 2 3 + TICK + ISWAP 0 1 2 3 4 5 + TICK + H 4 5 6 7 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + R 0 1 2 3 + X_ERROR(0.002) 0 1 2 3 + DEPOLARIZE1(0.0001) 4 5 6 7 + DEPOLARIZE1(0.002) 4 5 6 7 + TICK + ISWAP 0 1 2 3 4 5 + DEPOLARIZE2(0.001) 0 1 2 3 4 5 + DEPOLARIZE1(0.0001) 6 7 + TICK + H 4 5 6 7 + DEPOLARIZE1(0.0001) 4 5 6 7 0 1 2 3 + TICK + M(0.005) 0 1 2 3 + DEPOLARIZE1(0.001) 0 1 2 3 + DEPOLARIZE1(0.0001) 4 5 6 7 + DEPOLARIZE1(0.002) 4 5 6 7 + """ + ) + ) + + +def test_measure_any(): + model = NoiseModel( + any_clifford_1q_rule=stimflow.NoiseRule(after={}), + any_clifford_2q_rule=stimflow.NoiseRule(after={}), + any_measurement_rule=stimflow.NoiseRule(after={"DEPOLARIZE1": 0.125}, flip_result=0.25), + measure_rules={"XX": stimflow.NoiseRule(flip_result=0.375, after={})}, + ) + assert ( + model.noisy_circuit( + stim.Circuit( + """ + H 0 + TICK + CX 0 1 + TICK + M 0 1 + TICK + MPP Z0*Z1 X2*X3 X4*X5*X6 + """ + ) + ) + == stim.Circuit( + """ + H 0 + TICK + CX 0 1 + TICK + M(0.25) 0 1 + DEPOLARIZE1(0.125) 0 1 + TICK + MPP(0.25) Z0*Z1 + MPP(0.375) X2*X3 + MPP(0.25) X4*X5*X6 + DEPOLARIZE1(0.125) 0 1 4 5 6 + """ + ) + ) + + +def test_tick_depolarization(): + model = NoiseModel( + any_clifford_1q_rule=stimflow.NoiseRule(after={}), + any_clifford_2q_rule=stimflow.NoiseRule(after={}), + tick_noise=stimflow.NoiseRule(after={"DEPOLARIZE1": 0.375}), + any_measurement_rule=stimflow.NoiseRule(after={}), + ) + assert ( + model.noisy_circuit( + stim.Circuit( + """ + H 0 + TICK + CX 0 1 + TICK + M 0 1 + TICK + MPP Z0*Z1 X2*X3 X4*X5*X6 + """ + ) + ) + == stim.Circuit( + """ + H 0 + DEPOLARIZE1(0.375) 0 1 2 3 4 5 6 + TICK + CX 0 1 + DEPOLARIZE1(0.375) 0 1 2 3 4 5 6 + TICK + M 0 1 + DEPOLARIZE1(0.375) 0 1 2 3 4 5 6 + TICK + MPP Z0*Z1 X2*X3 X4*X5*X6 + DEPOLARIZE1(0.375) 0 1 2 3 4 5 6 + """ + ) + ) + + +def test_decomposed_gate_noise(): + assert ( + stimflow.NoiseModel(idle_depolarization=0.125, any_clifford_1q_rule=0.25).noisy_circuit( + stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + H 0 + X 0 + TICK + H 1 + """ + ) + ) + == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + H 0 + X 0 + DEPOLARIZE1(0.25) 0 + DEPOLARIZE1(0.125) 1 + TICK + H 1 + DEPOLARIZE1(0.25) 1 + DEPOLARIZE1(0.125) 0 + """ + ) + ) + + assert ( + stimflow.NoiseModel(idle_depolarization=0.125, any_clifford_1q_rule=0.25).noisy_circuit( + stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + X 0 + H 0 + TICK + H 1 + """ + ) + ) + == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + X 0 + H 0 + DEPOLARIZE1(0.25) 0 + DEPOLARIZE1(0.125) 1 + TICK + H 1 + DEPOLARIZE1(0.25) 1 + DEPOLARIZE1(0.125) 0 + """ + ) + ) + + assert ( + stimflow.NoiseModel(idle_depolarization=0.125, any_clifford_1q_rule=0.25).noisy_circuit( + stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + X 0 1 + H 0 + TICK + H 1 + """ + ) + ) + == stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + X 0 1 + H 0 + DEPOLARIZE1(0.25) 1 0 + TICK + H 1 + DEPOLARIZE1(0.25) 1 + DEPOLARIZE1(0.125) 0 + """ + ) + ) diff --git a/glue/stimflow/src/stimflow/_core/_pauli_map.py b/glue/stimflow/src/stimflow/_core/_pauli_map.py new file mode 100644 index 00000000..489ad115 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_pauli_map.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterable, Iterator, Set +from typing import Any, cast, Literal, TYPE_CHECKING + +import stim + +from stimflow._core._complex_util import sorted_complex + +if TYPE_CHECKING: + from stimflow._core._tile import Tile + + +_multiplication_table: dict[ + Literal["X", "Y", "Z"] | None, + dict[Literal["X", "Y", "Z"] | None, Literal["X", "Y", "Z"] | None], +] = { + None: {None: None, "X": "X", "Y": "Y", "Z": "Z"}, + "X": {None: "X", "X": None, "Y": "Z", "Z": "Y"}, + "Y": {None: "Y", "X": "Z", "Y": None, "Z": "X"}, + "Z": {None: "Z", "X": "Y", "Y": "X", "Z": None}, +} + + +class PauliMap: + """A qubit-to-pauli mapping.""" + + def __init__( + self, + mapping: ( + dict[complex, Literal["X", "Y", "Z"] | str] + | dict[Literal["X", "Y", "Z"] | str, complex | Iterable[complex]] + | PauliMap + | Tile + | stim.PauliString + | None + ) = None, + *, + name: Any = None, + ): + """Initializes a PauliMap using maps of Paulis to/from qubits. + + Args: + mapping: The association between qubits and paulis, specifiable in a variety of ways. + name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, + in order to identify the Pauli map. A common convention used in the library is that + named Pauli maps correspond to logical operators. + """ + + self.qubits: dict[complex, Literal["X", "Y", "Z"]] + self.name = name + self._hash: int + + from stimflow._core._tile import Tile + + if isinstance(mapping, Tile): + self.qubits = dict(mapping.to_pauli_map().qubits) + elif isinstance(mapping, PauliMap): + self.qubits = dict(mapping.qubits) + elif isinstance(mapping, stim.PauliString): + self.qubits = {q: cast(Any, "_XYZ"[mapping[q]]) for q in mapping.pauli_indices()} + elif mapping is not None: + self.qubits = {} + for k, v in mapping.items(): + if (v == "X" or v == "Y" or v == "Z") and isinstance(k, (int, float, complex)): + self._mul_term(k, cast(Any, v)) + elif (k == "X" or k == "Y" or k == "Z") and isinstance(v, (int, float, complex)): + self._mul_term(v, cast(Any, k)) + elif ( + (k == "X" or k == "Y" or k == "Z") + and isinstance(v, Iterable) + and (v_copy := list(v)) is not None + and all(isinstance(v2, (int, float, complex)) for v2 in v_copy) + ): + for v2 in v_copy: + self._mul_term(cast(Any, v2), cast(Any, k)) + else: + raise ValueError(f"Don't know how to interpret {k=}: {v=} as a pauli mapping.") + + self.qubits = {complex(q): self.qubits[q] for q in sorted_complex(self.keys())} + else: + self.qubits = {} + self._hash = hash((self.name, tuple(self.qubits.items()))) + + @staticmethod + def from_xs(xs: Iterable[complex], *, name: Any = None) -> PauliMap: + """Returns a PauliMap mapping the given qubits to the X basis.""" + return PauliMap({"X": xs}, name=name) + + @staticmethod + def from_ys(ys: Iterable[complex], *, name: Any = None) -> PauliMap: + """Returns a PauliMap mapping the given qubits to the Y basis.""" + return PauliMap({"Y": ys}, name=name) + + @staticmethod + def from_zs(zs: Iterable[complex], *, name: Any = None) -> PauliMap: + """Returns a PauliMap mapping the given qubits to the Z basis.""" + return PauliMap({"Z": zs}, name=name) + + def __contains__(self, item: complex) -> bool: + """Determines if the PauliMap contains maps the given qubit to a non-identity Pauli.""" + return self.qubits.__contains__(item) + + def items(self) -> Iterable[tuple[complex, Literal["X", "Y", "Z"]]]: + """Returns the (qubit, basis) pairs of the PauliMap.""" + return self.qubits.items() + + def values(self) -> Iterable[Literal["X", "Y", "Z"]]: + """Returns the bases used by the PauliMap.""" + return self.qubits.values() + + def keys(self) -> Set[complex]: + """Returns the qubits of the PauliMap.""" + return self.qubits.keys() + + def get(self, key: complex, default: Any = None) -> Any: + return self.qubits.get(key, default) + + def __getitem__(self, item: complex) -> Literal["I", "X", "Y", "Z"]: + return cast(Any, self.qubits.get(item, "I")) + + def __len__(self) -> int: + return len(self.qubits) + + def __iter__(self) -> Iterator[complex]: + return self.qubits.__iter__() + + def with_name(self, name: Any) -> PauliMap: + """Returns the same PauliMap, but with the given name. + + Names are used to identify logical operators. + """ + return PauliMap(self, name=name) + + def _mul_term(self, q: complex, b: Literal["X", "Y", "Z"]): + new_b = _multiplication_table[self.qubits.pop(q, None)][b] + if new_b is not None: + self.qubits[q] = new_b + + def with_basis(self, basis: Literal["X", "Y", "Z"]) -> PauliMap: + """Returns the same PauliMap, but with all its qubits mapped to the given basis.""" + return PauliMap({q: basis for q in self.keys()}, name=self.name) + + def __bool__(self) -> bool: + return bool(self.qubits) + + def __mul__(self, other: PauliMap | Tile) -> PauliMap: + from stimflow._core._tile import Tile + + if isinstance(other, Tile): + other = other.to_pauli_map() + + result: dict[complex, Literal["X", "Y", "Z"]] = {} + for q in self.keys() | other.keys(): + a = self.qubits.get(q, "I") + b = other.qubits.get(q, "I") + ax = a in "XY" + az = a in "YZ" + bx = b in "XY" + bz = b in "YZ" + cx = ax ^ bx + cz = az ^ bz + c = "IXZY"[cx + cz * 2] + if c != "I": + result[q] = cast(Literal["X", "Y", "Z"], c) + return PauliMap(result) + + def __repr__(self) -> str: + if self.name is None: + s2 = "" + else: + s2 = f", name={self.name!r}" + qs = sorted_complex(self.qubits) + if len(self) > 1: + p = set(self.values()) + if p == {'X'}: + return f"stimflow.PauliMap.from_xs({qs!r}{s2})" + if p == {'Z'}: + return f"stimflow.PauliMap.from_zs({qs!r}{s2})" + s = {q: self.qubits[q] for q in qs} + return f"stimflow.PauliMap({s!r}{s2})" + + def __str__(self) -> str: + def simplify(c: complex) -> str: + if c == int(c.real): + return str(int(c.real)) + if c == c.real: + return str(c.real) + return str(c) + + result = "*".join(f"{self.qubits[q]}{simplify(q)}" for q in sorted_complex(self.keys())) + if self.name is not None: + result = f"(name={self.name!r}) " + result + return result + + def with_xz_flipped(self) -> PauliMap: + """Returns the same PauliMap, but with all qubits conjugated by H.""" + remap = {"X": "Z", "Y": "Y", "Z": "X"} + return PauliMap({q: remap[p] for q, p in self.qubits.items()}, name=self.name) + + def with_xy_flipped(self) -> PauliMap: + """Returns the same PauliMap, but with all qubits conjugated by H_XY.""" + remap = {"X": "Y", "Y": "X", "Z": "Z"} + return PauliMap({q: remap[p] for q, p in self.qubits.items()}, name=self.name) + + def commutes(self, other: PauliMap) -> bool: + """Determines if the pauli map commutes with another pauli map.""" + return not self.anticommutes(other) + + def anticommutes(self, other: PauliMap) -> bool: + """Determines if the pauli map anticommutes with another pauli map.""" + t = 0 + for q in self.keys() & other.keys(): + t += self.qubits[q] != other.qubits[q] + return t % 2 == 1 + + def with_transformed_coords(self, transform: Callable[[complex], complex]) -> PauliMap: + """Returns the same PauliMap but with coordinates transformed by the given function.""" + return PauliMap({transform(q): p for q, p in self.qubits.items()}, name=self.name) + + def to_stim_pauli_string( + self, q2i: dict[complex, int], *, num_qubits: int | None = None + ) -> stim.PauliString: + """Converts into a stim.PauliString.""" + if num_qubits is None: + num_qubits = max([q2i[q] + 1 for q in self.keys()], default=0) + result = stim.PauliString(num_qubits) + for q, p in self.items(): + result[q2i[q]] = p + return result + + def to_stim_targets(self, q2i: dict[complex, int]) -> list[stim.GateTarget]: + """Converts into a stim combined pauli target like 'X1*Y2*Z3'.""" + assert len(self) > 0 + + targets = [] + for q, p in self.items(): + targets.append(stim.target_pauli(q2i[q], p)) + targets.append(stim.target_combiner()) + targets.pop() + return targets + + def to_tile(self) -> Tile: + """Converts the PauliMap into a stimflow.Tile.""" + from stimflow._core._tile import Tile + + qs = list(self.keys()) + return Tile(bases="".join(self.values()), data_qubits=qs) + + def __hash__(self) -> int: + return self._hash + + def __eq__(self, other) -> bool: + if not isinstance(other, PauliMap): + return NotImplemented + return self.qubits == other.qubits + + def _sort_key(self) -> Any: + return tuple((q.real, q.imag, p) for q, p in self.qubits.items()) + + def __lt__(self, other) -> bool: + if not isinstance(other, PauliMap): + return NotImplemented + return self._sort_key() < other._sort_key() diff --git a/glue/stimflow/src/stimflow/_core/_pauli_map_test.py b/glue/stimflow/src/stimflow/_core/_pauli_map_test.py new file mode 100644 index 00000000..78171daa --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_pauli_map_test.py @@ -0,0 +1,19 @@ +import stim + +import stimflow + + +def test_mul(): + a = "IIIIXXXXYYYYZZZZ" + b = "IXYZ" * 4 + c = "IXYZXIZYYZIXZYXI" + a = stimflow.PauliMap({q: p for q, p in enumerate(a) if p != "I"}) + b = stimflow.PauliMap({q: p for q, p in enumerate(b) if p != "I"}) + c = stimflow.PauliMap({q: p for q, p in enumerate(c) if p != "I"}) + assert a * b == c + + +def test_init(): + assert stimflow.PauliMap(stim.PauliString("_XYZ_XX")) == stimflow.PauliMap( + {"X": [1, 5, 6], "Y": [2], "Z": [3]} + ) diff --git a/glue/stimflow/src/stimflow/_core/_str_html.py b/glue/stimflow/src/stimflow/_core/_str_html.py new file mode 100644 index 00000000..6cf1c81d --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_str_html.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import io +import pathlib +import sys + + +class str_html(str): + """A string that will display as an HTML widget in Jupyter notebooks. + + It's expected that the contents of the string will correspond to the + contents of an HTML file. + """ + + def __str__(self) -> str: + """Strips down to a bare string.""" + return self.encode("utf-8").decode("utf-8") + + def _repr_html_(self) -> str: + """This is the method Jupyter notebooks look for, to show as HTML.""" + return self + + def write_to(self, path: str | pathlib.Path | io.IOBase): + """Write the contents to a file, and announce that it was done. + + This method exists for quick debugging. In many contexts, such as + in a bash terminal or in PyCharm, the printed path can be clicked + on to open the file. + """ + if isinstance(path, io.IOBase): + path.write(self) + return + path = pathlib.Path(path) + path.parent.mkdir(exist_ok=True, parents=True) + if isinstance(self, bytes): + with open(path, "wb") as f: + print(self, file=f) + else: + with open(path, "w") as f: + print(self, file=f) + print(f"wrote file://{pathlib.Path(path).absolute()}", file=sys.stderr) diff --git a/glue/stimflow/src/stimflow/_core/_str_svg.py b/glue/stimflow/src/stimflow/_core/_str_svg.py new file mode 100644 index 00000000..492bfe69 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_str_svg.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import io +import pathlib +import sys + + +class str_svg(str): + """A string that will display as an SVG image in Jupyter notebooks. + + It's expected that the contents of the string will correspond to the + contents of an SVG file. + """ + + def __str__(self) -> str: + """Strips down to a bare string.""" + return self.encode("utf-8").decode("utf-8") + + def _repr_svg_(self) -> str: + """This is the method Jupyter notebooks look for, to show as an SVG.""" + return self + + def write_to(self, path: str | pathlib.Path | io.IOBase): + """Write the contents to a file, and announce that it was done. + + This method exists for quick debugging. In many contexts, such as + in a bash terminal or in PyCharm, the printed path can be clicked + on to open the file. + """ + if isinstance(path, io.IOBase): + path.write(self) + return + path = pathlib.Path(path) + path.parent.mkdir(exist_ok=True, parents=True) + if isinstance(self, bytes): + with open(path, "wb") as f: + print(self, file=f) + else: + with open(path, "w") as f: + print(self, file=f) + print(f"wrote file://{pathlib.Path(path).absolute()}", file=sys.stderr) diff --git a/glue/stimflow/src/stimflow/_core/_tile.py b/glue/stimflow/src/stimflow/_core/_tile.py new file mode 100644 index 00000000..9bc27a20 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_tile.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import functools +from collections.abc import Callable, Iterable +from typing import Any, cast, Literal + +from stimflow._core._pauli_map import PauliMap + + +class Tile: + """A stabilizer with some associated metadata. + + The exact meaning of the tile's fields are often context dependent. For example, + different circuits will use the measure qubit in different ways (or not at all) + and the flags could be essentially anything at all. Tile is intended to be useful + as an intermediate step in the production of a circuit. + + For example, it's much easier to create a color code circuit when you have a list + of the hexagonal and trapezoidal shapes making up the color code. So it's natural to + split the color code circuit generation problem into two steps: (1) making the shapes + then (2) making the circuit given the shapes. In other words, deal with the spatial + complexities first then deal with the temporal complexities second. The Tile class + is a reasonable representation for the shapes, because: + + - The X/Z basis of the stabilizer can be stored in the `bases` field. + - The red/green/blue coloring can be stored as flags. + - The ancilla qubits for the shapes be stored as measure_qubit values. + - You can get diagrams of the shapes by passing the tiles into a `stimflow.Patch`. + - You can verify the tiles form a code by passing the patch into a `stimflow.StabilizerCode`. + """ + + def __init__( + self, + *, + bases: str, + data_qubits: Iterable[complex | None], + measure_qubit: complex | None = None, + flags: Iterable[str] = (), + ): + """ + + Args: + bases: Basis of the stabilizer. A string of XYZ characters the same + length as the data_qubits argument. It is permitted to + give a single-character string, which will automatically be + expanded to the full length. For example, 'X' will become 'XXXX' + if there are four data qubits. + measure_qubit: The ancilla qubit used to measure the stabilizer. + data_qubits: The data qubits in the stabilizer, in the order + that they are interacted with. Some entries may be None, + indicating that no data qubit is interacted with during the + corresponding interaction layer. + """ + assert isinstance(bases, str) + self.data_qubits = tuple(data_qubits) + self.measure_qubit: complex | None = measure_qubit + if len(bases) == 1: + bases *= len(self.data_qubits) + self.bases: str = bases + self.flags: frozenset[str] = frozenset(flags) + if len(self.bases) != len(self.data_qubits): + raise ValueError("len(self.bases_2) != len(self.data_qubits_order)") + + def center(self) -> complex: + if self.measure_qubit is not None: + return self.measure_qubit + if self.data_set: + return sum(self.data_set) / len(self.data_set) + return 0 + + def _cmp_key(self) -> Any: + return ( + self.center().real, + self.center().imag, + self.to_pauli_map(), + tuple(sorted(self.flags)), + ) + + def __eq__(self, other): + if not isinstance(other, Tile): + return False + return ( + self.data_qubits == other.data_qubits + and self.measure_qubit == other.measure_qubit + and self.bases == other.bases + and self.flags == other.flags + ) + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Tile): + return self._cmp_key() < other._cmp_key() + return NotImplemented + + def __ne__(self, other: Any) -> bool: + return not (self == other) + + def __hash__(self) -> int: + return hash((Tile, self.data_qubits, self.measure_qubit, self.bases, self.flags)) + + def __repr__(self) -> str: + b = self.basis or self.bases + extra = "" if not self.flags else f"\n flags={sorted(self.flags)!r}," + return f"""stimflow.Tile( + data_qubits={self.data_qubits!r}, + measure_qubit={self.measure_qubit!r}, + bases={b!r},{extra} +)""" + + def to_pauli_map(self) -> PauliMap: + return PauliMap({q: b for q, b in zip(self.data_qubits, self.bases) if q is not None}) + + def with_data_qubit_cleared(self, q: complex) -> Tile: + return self.with_edits(data_qubits=[None if d == q else d for d in self.data_qubits]) + + def with_edits( + self, + *, + bases: str | None = None, + measure_qubit: complex | None | Literal["unspecified"] = "unspecified", + data_qubits: Iterable[complex | None] | None = None, + flags: Iterable[str] | None = None, + ) -> Tile: + if data_qubits is not None: + data_qubits = tuple(data_qubits) + if len(data_qubits) != len(self.data_qubits) and bases is None: + if self.basis is None: + raise ValueError("Changed data qubit count of non-uniform basis tile.") + bases = self.basis + + return Tile( + bases=self.bases if bases is None else bases, + measure_qubit=self.measure_qubit if measure_qubit == "unspecified" else measure_qubit, + data_qubits=self.data_qubits if data_qubits is None else data_qubits, + flags=self.flags if flags is None else flags, + ) + + def with_bases(self, bases: str) -> Tile: + return self.with_edits(bases=bases) + + with_basis = with_bases + + def with_xz_flipped(self) -> Tile: + f = {"X": "Z", "Y": "Y", "Z": "X"} + return self.with_bases("".join(f[e] for e in self.bases)) + + def with_transformed_coords(self, coord_transform: Callable[[complex], complex]) -> Tile: + return self.with_edits( + data_qubits=[None if d is None else coord_transform(d) for d in self.data_qubits], + measure_qubit=( + None if self.measure_qubit is None else coord_transform(self.measure_qubit) + ), + ) + + def with_transformed_bases( + self, basis_transform: Callable[[Literal["X", "Y", "Z"]], Literal["X", "Y", "Z"]] + ) -> Tile: + return self.with_bases( + "".join(basis_transform(cast(Literal["X", "Y", "Z"], e)) for e in self.bases) + ) + + def __len__(self) -> int: + return len(self.data_set) + + @functools.cached_property + def data_set(self) -> frozenset[complex]: + return frozenset(e for e in self.data_qubits if e is not None) + + @functools.cached_property + def used_set(self) -> frozenset[complex]: + if self.measure_qubit is None: + return self.data_set + return self.data_set | frozenset([self.measure_qubit]) + + @functools.cached_property + def basis(self) -> Literal["X", "Y", "Z"] | None: + bs: set[Literal["X", "Y", "Z"]] + bs = cast(Any, {b for q, b in zip(self.data_qubits, self.bases) if q is not None}) + if len(bs) == 0: + # Fallback to including ejected qubits. + bs = cast(Any, set(self.bases)) + if len(bs) != 1: + return None + return next(iter(bs)) diff --git a/glue/stimflow/src/stimflow/_core/_tile_test.py b/glue/stimflow/src/stimflow/_core/_tile_test.py new file mode 100644 index 00000000..5d74c0bb --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_tile_test.py @@ -0,0 +1,18 @@ +from stimflow._core._tile import Tile + + +def test_basis(): + tile = Tile(bases="XYZX", measure_qubit=0, data_qubits=(1, 2, None, 3)) + assert tile.basis is None + + tile = Tile(bases="XXZX", measure_qubit=0, data_qubits=(1, 2, None, 3)) + assert tile.basis == "X" + + tile = Tile(bases="XXX", measure_qubit=0, data_qubits=(1, 2, 3)) + assert tile.basis == "X" + + tile = Tile(bases="ZZZ", measure_qubit=0, data_qubits=(1, 2, 3)) + assert tile.basis == "Z" + + tile = Tile(bases="ZXZ", measure_qubit=0, data_qubits=(1, 2, 3)) + assert tile.basis is None diff --git a/glue/stimflow/src/stimflow/_layers/__init__.py b/glue/stimflow/src/stimflow/_layers/__init__.py new file mode 100644 index 00000000..643c3f46 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/__init__.py @@ -0,0 +1,8 @@ +"""Works with circuits in a layered representation that's easy to operate on.""" + +from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer_circuit import LayerCircuit +from stimflow._layers._measure_layer import MeasureLayer +from stimflow._layers._reset_layer import ResetLayer +from stimflow._layers._rotation_layer import RotationLayer +from stimflow._layers._transpile import transpile_to_z_basis_interaction_circuit diff --git a/glue/stimflow/src/stimflow/_layers/_data.py b/glue/stimflow/src/stimflow/_layers/_data.py new file mode 100644 index 00000000..1d23d63c --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_data.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import functools +from typing import Any, cast, Literal + +import stim + + +def _single_qubit_tableau_to_key(t: stim.Tableau) -> str: + return f"{t.x_output(0)}{t.z_output(0)}" + + +@functools.cache +def gate_to_unsigned_pauli_change_inverse() -> ( + dict[str, dict[Literal["X", "Y", "Z"], Literal["X", "Y", "Z"]]] +): + return { + gate: {v: k for k, v in items.items()} + for gate, items in gate_to_unsigned_pauli_change().items() + } + + +@functools.cache +def gate_to_unsigned_pauli_change() -> ( + dict[str, dict[Literal["X", "Y", "Z"], Literal["X", "Y", "Z"]]] +): + result: dict[str, dict[Literal["X", "Y", "Z"], Literal["X", "Y", "Z"]]] = {} + for vals, gate in keyed_single_qubit_cliffords().items(): + _x_sign: Literal["-", "+"] + x_out: Literal["X", "Y", "Z"] + _z_sign: Literal["-", "+"] + z_out: Literal["X", "Y", "Z"] + _x_sign, x_out, _z_sign, z_out = cast(Any, vals) + (y_out,) = set("XYZ") - {x_out, z_out} + result[gate] = cast(Any, {"X": x_out, "Z": z_out, "Y": y_out}) + return result + + +@functools.cache +def keyed_single_qubit_cliffords() -> dict[str, str]: + tableau_to_gate_name = {} + + # Find basic gates. + for gate in stim.gate_data().values(): + if gate.is_single_qubit_gate and gate.is_unitary: + tableau_to_gate_name[_single_qubit_tableau_to_key(gate.tableau)] = gate.name + + # Form remaining composite gates. + for g in ["H", "S", "SQRT_X", "C_XYZ", "C_ZYX"]: + gt = stim.gate_data(g).tableau + for p in "XZY": + pt = stim.gate_data(p).tableau + k2 = _single_qubit_tableau_to_key(pt * gt) + if k2 not in tableau_to_gate_name: + tableau_to_gate_name[k2] = p + "*" + g + + return tableau_to_gate_name + + +@functools.cache +def single_qubit_clifford_inverse_table() -> dict[str, str]: + m = keyed_single_qubit_cliffords() + inverse_table = {} + + for t1 in stim.Tableau.iter_all(num_qubits=1): + k1 = _single_qubit_tableau_to_key(t1) + k2 = _single_qubit_tableau_to_key(t1**-1) + inverse_table[m[k1]] = m[k2] + + return inverse_table + + +@functools.cache +def single_qubit_clifford_multiplication_table() -> dict[tuple[str, str], str]: + m = keyed_single_qubit_cliffords() + + # Compute the multiplication table. + multiplication_table = {} + tableaus = list(stim.Tableau.iter_all(num_qubits=1)) + for t1 in tableaus: + k1 = _single_qubit_tableau_to_key(t1) + g1 = m[k1] + for t2 in tableaus: + k2 = _single_qubit_tableau_to_key(t2) + g2 = m[k2] + t3 = t1 * t2 + k3 = _single_qubit_tableau_to_key(t3) + g3 = m[k3] + multiplication_table[(g1, g2)] = g3 + + return multiplication_table diff --git a/glue/stimflow/src/stimflow/_layers/_data_test.py b/glue/stimflow/src/stimflow/_layers/_data_test.py new file mode 100644 index 00000000..b0ffe37a --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_data_test.py @@ -0,0 +1,24 @@ +from stimflow._layers._data import ( + gate_to_unsigned_pauli_change, + gate_to_unsigned_pauli_change_inverse, + single_qubit_clifford_multiplication_table, +) + + +def test_single_qubit_clifford_multiplication_table(): + v = single_qubit_clifford_multiplication_table() + assert len(v) == 24 * 24 + + +def test_gate_to_unsigned_pauli_change(): + m = gate_to_unsigned_pauli_change() + assert m["H"] == {"X": "Z", "Z": "X", "Y": "Y"} + assert m["C_XYZ"] == {"X": "Y", "Y": "Z", "Z": "X"} + assert m["C_ZYX"] == {"X": "Z", "Z": "Y", "Y": "X"} + assert m["C_NZYX"] == {"X": "Z", "Z": "Y", "Y": "X"} + + m = gate_to_unsigned_pauli_change_inverse() + assert m["H"] == {"X": "Z", "Z": "X", "Y": "Y"} + assert m["C_ZYX"] == {"X": "Y", "Y": "Z", "Z": "X"} + assert m["C_XYZ"] == {"X": "Z", "Z": "Y", "Y": "X"} + assert m["C_NXYZ"] == {"X": "Z", "Z": "Y", "Y": "X"} diff --git a/glue/stimflow/src/stimflow/_layers/_det_obs_annotation_layer.py b/glue/stimflow/src/stimflow/_layers/_det_obs_annotation_layer.py new file mode 100644 index 00000000..623f29f4 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_det_obs_annotation_layer.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class DetObsAnnotationLayer(Layer): + circuit: stim.Circuit = dataclasses.field(default_factory=stim.Circuit) + + def with_rec_targets_shifted_by(self, shift: int) -> DetObsAnnotationLayer: + result = DetObsAnnotationLayer() + for inst in self.circuit: + result.circuit.append( + inst.name, + [stim.target_rec(t.value + shift) for t in inst.targets_copy()], + inst.gate_args_copy(), + ) + return result + + def copy(self) -> DetObsAnnotationLayer: + return DetObsAnnotationLayer(circuit=self.circuit.copy()) + + def touched(self) -> set[int]: + return set() + + def requires_tick_before(self) -> bool: + return False + + def implies_eventual_tick_after(self) -> bool: + return False + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + out += self.circuit + + def locally_optimized(self, next_layer: None | Layer) -> list[Layer | None]: + if isinstance(next_layer, DetObsAnnotationLayer): + return [DetObsAnnotationLayer(self.circuit + next_layer.circuit)] + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_empty_layer.py b/glue/stimflow/src/stimflow/_layers/_empty_layer.py new file mode 100644 index 00000000..06d91690 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_empty_layer.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class EmptyLayer(Layer): + def copy(self) -> EmptyLayer: + return EmptyLayer() + + def touched(self) -> set[int]: + return set() + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + pass + + def locally_optimized(self, next_layer: None | Layer) -> list[Layer | None]: + return [next_layer] + + def is_vacuous(self) -> bool: + return True diff --git a/glue/stimflow/src/stimflow/_layers/_feedback_layer.py b/glue/stimflow/src/stimflow/_layers/_feedback_layer.py new file mode 100644 index 00000000..eaa00807 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_feedback_layer.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import dataclasses +from typing import Literal, TYPE_CHECKING + +import stim + +from stimflow._layers._data import gate_to_unsigned_pauli_change_inverse +from stimflow._layers._layer import Layer + +if TYPE_CHECKING: + from stimflow._layers._rotation_layer import RotationLayer + + +@dataclasses.dataclass +class FeedbackLayer(Layer): + controls: list[stim.GateTarget] = dataclasses.field(default_factory=list) + targets: list[int] = dataclasses.field(default_factory=list) + bases: list[Literal["X", "Y", "Z"]] = dataclasses.field(default_factory=list) + + def with_rec_targets_shifted_by(self, shift: int) -> FeedbackLayer: + result = self.copy() + result.controls = [stim.target_rec(t.value + shift) for t in result.controls] + return result + + def copy(self) -> FeedbackLayer: + return FeedbackLayer( + targets=list(self.targets), controls=list(self.controls), bases=list(self.bases) + ) + + def touched(self) -> set[int]: + return set(self.targets) + + def requires_tick_before(self) -> bool: + return False + + def implies_eventual_tick_after(self) -> bool: + return False + + def before(self, layer: RotationLayer) -> FeedbackLayer: + return FeedbackLayer( + controls=list(self.controls), + targets=list(self.targets), + bases=[ + _basis_before_rotation(b, layer.named_rotations.get(t, "I")) + for b, t in zip(self.bases, self.targets) + ], + ) + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + for c, t, b in zip(self.controls, self.targets, self.bases): + out.append("C" + b, [c, t]) + + +def _basis_before_rotation( + basis: Literal["X", "Y", "Z"], named_rotation: str +) -> Literal["X", "Y", "Z"]: + return gate_to_unsigned_pauli_change_inverse()[named_rotation][basis] diff --git a/glue/stimflow/src/stimflow/_layers/_feedback_layer_test.py b/glue/stimflow/src/stimflow/_layers/_feedback_layer_test.py new file mode 100644 index 00000000..9aef7b43 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_feedback_layer_test.py @@ -0,0 +1,7 @@ +from stimflow._layers._feedback_layer import _basis_before_rotation + + +def test_basis_before_rotation(): + assert _basis_before_rotation("X", "C_ZYX") == "Y" + assert _basis_before_rotation("Y", "C_ZYX") == "Z" + assert _basis_before_rotation("Z", "C_ZYX") == "X" diff --git a/glue/stimflow/src/stimflow/_layers/_interact_layer.py b/glue/stimflow/src/stimflow/_layers/_interact_layer.py new file mode 100644 index 00000000..b136c710 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_interact_layer.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import collections +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class InteractLayer(Layer): + """A layer of controlled Pauli gates (like CX, CZ, and XCY).""" + + targets1: list[int] = dataclasses.field(default_factory=list) + targets2: list[int] = dataclasses.field(default_factory=list) + bases1: list[str] = dataclasses.field(default_factory=list) + bases2: list[str] = dataclasses.field(default_factory=list) + + def touched(self) -> set[int]: + return set(self.targets1 + self.targets2) + + def copy(self) -> InteractLayer: + return InteractLayer( + targets1=list(self.targets1), + targets2=list(self.targets2), + bases1=list(self.bases1), + bases2=list(self.bases2), + ) + + def rotate_to_z_layer(self): + from stimflow._layers._rotation_layer import RotationLayer + + result = RotationLayer() + for targets, bases in [(self.targets1, self.bases1), (self.targets2, self.bases2)]: + for q, b in zip(targets, bases): + if b == "X": + result.named_rotations[q] = "H" + elif b == "Y": + result.named_rotations[q] = "H_YZ" + return result + + def to_z_basis(self) -> list[Layer]: + rot = self.rotate_to_z_layer() + return [ + rot, + InteractLayer( + targets1=list(self.targets1), + targets2=list(self.targets2), + bases1=["Z"] * len(self.targets1), + bases2=["Z"] * len(self.targets2), + ), + rot.copy(), + ] + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + groups = collections.defaultdict(list) + for k in range(len(self.targets1)): + gate = self.bases1[k] + "C" + self.bases2[k] + t1 = self.targets1[k] + t2 = self.targets2[k] + if gate in ["XCZ", "YCZ", "YCX"]: + t1, t2 = t2, t1 + gate = gate[::-1] + if gate in ["XCX", "YCY", "ZCZ"]: + t1, t2 = sorted([t1, t2]) + groups[gate].append((t1, t2)) + for gate in sorted(groups.keys()): + for pair in sorted(groups[gate]): + out.append(gate, pair) + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + from stimflow._layers._interact_swap_layer import InteractSwapLayer + from stimflow._layers._swap_layer import SwapLayer + + if isinstance(next_layer, SwapLayer): + pairs1 = {frozenset([a, b]) for a, b in zip(self.targets1, self.targets2)} + pairs2 = {frozenset([a, b]) for a, b in zip(next_layer.targets1, next_layer.targets2)} + if pairs1 == pairs2: + return [InteractSwapLayer(i_layer=self.copy())] + elif isinstance(next_layer, InteractLayer) and self.touched().isdisjoint( + next_layer.touched() + ): + return [ + InteractLayer( + targets1=self.targets1 + next_layer.targets1, + targets2=self.targets2 + next_layer.targets2, + bases1=self.bases1 + next_layer.bases1, + bases2=self.bases2 + next_layer.bases2, + ) + ] + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_interact_swap_layer.py b/glue/stimflow/src/stimflow/_layers/_interact_swap_layer.py new file mode 100644 index 00000000..94f711a1 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_interact_swap_layer.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer import Layer +from stimflow._layers._swap_layer import SwapLayer + + +@dataclasses.dataclass +class InteractSwapLayer(Layer): + i_layer: InteractLayer = dataclasses.field(default_factory=InteractLayer) + + def copy(self) -> InteractSwapLayer: + return InteractSwapLayer(i_layer=self.i_layer.copy()) + + def touched(self) -> set[int]: + return self.i_layer.touched() + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + cz_swaps = [] + cx_swaps = [] + reduced_layer = InteractLayer() + for t1, t2, b1, b2 in zip( + self.i_layer.targets1, self.i_layer.targets2, self.i_layer.bases1, self.i_layer.bases2 + ): + if b1 == b2 == "Z": + if t2 < t1: + t1, t2 = t2, t1 + cz_swaps.append(t1) + cz_swaps.append(t2) + elif b1 == "X" and b2 == "Z": + cx_swaps.append(t2) + cx_swaps.append(t1) + elif b2 == "X" and b1 == "Z": + cx_swaps.append(t1) + cx_swaps.append(t2) + else: + reduced_layer.targets1.append(t1) + reduced_layer.targets2.append(t2) + reduced_layer.bases1.append(b1) + reduced_layer.bases2.append(b2) + + if cx_swaps: + out.append("CXSWAP", cx_swaps) + if cz_swaps: + out.append("CZSWAP", cz_swaps) + if reduced_layer.targets1: + reduced_layer.append_into_stim_circuit(out) + out.append("TICK") + SwapLayer(reduced_layer.targets1, reduced_layer.targets2).append_into_stim_circuit(out) + + def to_z_basis(self) -> list[Layer]: + return [ + self.i_layer.rotate_to_z_layer(), + InteractSwapLayer( + InteractLayer( + targets1=list(self.i_layer.targets1), + targets2=list(self.i_layer.targets2), + bases1=["Z"] * len(self.i_layer.bases1), + bases2=["Z"] * len(self.i_layer.bases2), + ) + ), + InteractLayer( + targets1=self.i_layer.targets1, + targets2=self.i_layer.targets2, + bases1=self.i_layer.bases2, + bases2=self.i_layer.bases1, + ).rotate_to_z_layer(), + ] + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_interact_swap_layer_test.py b/glue/stimflow/src/stimflow/_layers/_interact_swap_layer_test.py new file mode 100644 index 00000000..4f6f6177 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_interact_swap_layer_test.py @@ -0,0 +1,59 @@ +import stim + +from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._interact_swap_layer import InteractSwapLayer +from stimflow._layers._rotation_layer import RotationLayer + + +def test_to_z_basis(): + layer = InteractSwapLayer( + i_layer=InteractLayer( + targets1=[0, 2, 4], targets2=[1, 3, 5], bases1=["X", "Z", "Z"], bases2=["Y", "X", "Z"] + ) + ) + v = layer.to_z_basis() + assert v == [ + RotationLayer({0: "H", 1: "H_YZ", 3: "H"}), + InteractSwapLayer( + i_layer=InteractLayer( + targets1=[0, 2, 4], + targets2=[1, 3, 5], + bases1=["Z", "Z", "Z"], + bases2=["Z", "Z", "Z"], + ) + ), + RotationLayer({0: "H_YZ", 1: "H", 2: "H"}), + ] + + +def test_append_into_circuit(): + layer = InteractSwapLayer( + i_layer=InteractLayer( + targets1=[0, 2, 4], targets2=[1, 3, 5], bases1=["X", "Z", "Z"], bases2=["Y", "X", "Z"] + ) + ) + circuit = stim.Circuit() + layer.append_into_stim_circuit(circuit) + assert circuit == stim.Circuit( + """ + CXSWAP 2 3 + CZSWAP 4 5 + XCY 0 1 + TICK + SWAP 0 1 + """ + ) + + layer = InteractSwapLayer( + i_layer=InteractLayer( + targets1=[0, 2, 4], targets2=[1, 3, 5], bases1=["Z", "Z", "Z"], bases2=["Z", "X", "Z"] + ) + ) + circuit = stim.Circuit() + layer.append_into_stim_circuit(circuit) + assert circuit == stim.Circuit( + """ + CXSWAP 2 3 + CZSWAP 0 1 4 5 + """ + ) diff --git a/glue/stimflow/src/stimflow/_layers/_iswap_layer.py b/glue/stimflow/src/stimflow/_layers/_iswap_layer.py new file mode 100644 index 00000000..b3846c80 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_iswap_layer.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class ISwapLayer(Layer): + """A layer of iswap gates.""" + + targets1: list[int] = dataclasses.field(default_factory=list) + targets2: list[int] = dataclasses.field(default_factory=list) + + def copy(self) -> ISwapLayer: + return ISwapLayer(targets1=list(self.targets1), targets2=list(self.targets2)) + + def touched(self) -> set[int]: + return set(self.targets1 + self.targets2) + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + pairs = [] + for k in range(len(self.targets1)): + t1 = self.targets1[k] + t2 = self.targets2[k] + t1, t2 = sorted([t1, t2]) + pairs.append((t1, t2)) + for pair in sorted(pairs): + out.append("ISWAP", pair) + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_layer.py b/glue/stimflow/src/stimflow/_layers/_layer.py new file mode 100644 index 00000000..275186c0 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import stim + + +class Layer: + def copy(self) -> Layer: + """Returns an independent copy of the layer.""" + raise NotImplementedError() + + def touched(self) -> set[int]: + """Returns the set of qubit indices touched by the layer.""" + raise NotImplementedError() + + def to_z_basis(self) -> list[Layer]: + """Decomposes into a series of layers do the same thing with Z basis interactions. + + For example, it should use: + - CZ instead of CX + - CZSWAP instead of CXSWAP + - MZ instead of MX + - MZZ instead of MXX + - etc + + This will typically be achieved by adding rotation layers before/afterward. + """ + return [self] + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + """Appends the layer's contents into the given stim circuit.""" + raise NotImplementedError() + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + """Returns an equivalent series of layers that has been optimized. + + For example, if this is a RotationLayer and next_layer is also a RotationLayer, + then the result will be a single merged RotationLayer. + """ + return [self, next_layer] + + def is_vacuous(self) -> bool: + """Returns True if the layer doesn't do anything. + + For example, a RotationLayer with no rotations is vacuous. + """ + return False + + def requires_tick_before(self) -> bool: + """Returns True if the layer should be separated from the preceding layer.""" + return True + + def implies_eventual_tick_after(self) -> bool: + """Returns True if the layer should take time to perform.""" + return True diff --git a/glue/stimflow/src/stimflow/_layers/_layer_circuit.py b/glue/stimflow/src/stimflow/_layers/_layer_circuit.py new file mode 100644 index 00000000..0df225e6 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_circuit.py @@ -0,0 +1,808 @@ +from __future__ import annotations + +import dataclasses +from typing import Any, cast, Literal, TypeVar + +import stim + +from stimflow._layers._det_obs_annotation_layer import DetObsAnnotationLayer +from stimflow._layers._empty_layer import EmptyLayer +from stimflow._layers._feedback_layer import FeedbackLayer +from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._interact_swap_layer import InteractSwapLayer +from stimflow._layers._iswap_layer import ISwapLayer +from stimflow._layers._layer import Layer +from stimflow._layers._loop_layer import LoopLayer +from stimflow._layers._measure_layer import MeasureLayer +from stimflow._layers._mpp_layer import MppLayer +from stimflow._layers._noise_layer import NoiseLayer +from stimflow._layers._qubit_coord_annotation_layer import QubitCoordAnnotationLayer +from stimflow._layers._reset_layer import ResetLayer +from stimflow._layers._rotation_layer import RotationLayer +from stimflow._layers._shift_coord_annotation_layer import ShiftCoordAnnotationLayer +from stimflow._layers._sqrt_pp_layer import SqrtPPLayer +from stimflow._layers._swap_layer import SwapLayer +from stimflow._layers._tag_layer import TagLayer + +TLayer = TypeVar("TLayer") + + +@dataclasses.dataclass +class LayerCircuit: + """A stabilizer circuit represented as a series of typed layers. + + For example, the circuit could be a `ResetLayer`, then a `RotationLayer`, + then a few `InteractLayer`s, then a `MeasureLayer`. + """ + + layers: list[Layer] = dataclasses.field(default_factory=list) + + def touched(self) -> set[int]: + result = set() + for layer in self.layers: + result |= layer.touched() + return result + + def copy(self) -> LayerCircuit: + return LayerCircuit(layers=[e.copy() for e in self.layers]) + + def to_z_basis(self) -> LayerCircuit: + result = LayerCircuit() + for layer in self.layers: + result.layers.extend(layer.to_z_basis()) + return result + + def _feed(self, kind: type[TLayer]) -> TLayer: + if not self.layers: + self.layers.append(cast(Layer, kind())) + elif isinstance(self.layers[-1], EmptyLayer): + self.layers[-1] = cast(Layer, kind()) + elif not isinstance(self.layers[-1], kind): + self.layers.append(cast(Layer, kind())) + return cast(TLayer, self.layers[-1]) + + def _feed_reset(self, basis: Literal["X", "Y", "Z"], targets: list[stim.GateTarget]): + layer = self._feed(ResetLayer) + for t in targets: + layer.targets[t.value] = basis + + def _feed_tag(self, instruction: stim.CircuitInstruction): + layer = self._feed(TagLayer) + layer.circuit.append(instruction) + + def _feed_m(self, basis: Literal["X", "Y", "Z"], targets: list[stim.GateTarget]): + layer = self._feed(MeasureLayer) + for t in targets: + layer.bases.append(basis) + layer.targets.append(t.value) + + def _feed_mpp(self, targets: list[stim.GateTarget]): + layer = self._feed(MppLayer) + start = 0 + end = 1 + while start < len(targets): + while end < len(targets) and targets[end].is_combiner: + end += 2 + layer.targets.append(targets[start:end:2]) + start = end + end += 1 + + def _feed_qubit_coords(self, targets: list[stim.GateTarget], gate_args: list[float]): + layer = self._feed(QubitCoordAnnotationLayer) + for target in targets: + assert target.is_qubit_target + q = target.value + if q in layer.coords: + raise ValueError(f"Qubit coords specified twice for {q}") + layer.coords[q] = list(gate_args) + + def _feed_shift_coords(self, gate_args: list[float]): + self._feed(ShiftCoordAnnotationLayer).offset_by(gate_args) + + def _feed_named_rotation_instruction(self, instruction: stim.CircuitInstruction): + layer = self._feed(RotationLayer) + name = instruction.name + for t in instruction.targets_copy(): + layer.append_named_rotation(name, t.value) + + def _feed_swap(self, targets: list[stim.GateTarget]): + layer = self._feed(SwapLayer) + for k in range(0, len(targets), 2): + layer.targets1.append(targets[k].value) + layer.targets2.append(targets[k + 1].value) + + def _feed_cxswap(self, targets: list[stim.GateTarget]): + layer: InteractSwapLayer = self._feed(InteractSwapLayer) + for k in range(0, len(targets), 2): + layer.i_layer.targets1.append(targets[k].value) + layer.i_layer.targets2.append(targets[k + 1].value) + layer.i_layer.bases1.append("Z") + layer.i_layer.bases2.append("X") + + def _feed_swapcx(self, targets: list[stim.GateTarget]): + layer: InteractSwapLayer = self._feed(InteractSwapLayer) + for k in range(0, len(targets), 2): + layer.i_layer.targets1.append(targets[k].value) + layer.i_layer.targets2.append(targets[k + 1].value) + layer.i_layer.bases1.append("X") + layer.i_layer.bases2.append("Z") + + def _feed_iswap(self, targets: list[stim.GateTarget]): + layer = self._feed(ISwapLayer) + for k in range(0, len(targets), 2): + layer.targets1.append(targets[k].value) + layer.targets2.append(targets[k + 1].value) + + def _feed_sqrt_pp(self, basis: Literal["X", "Y", "Z"], targets: list[stim.GateTarget]): + layer = self._feed(SqrtPPLayer) + for k in range(0, len(targets), 2): + layer.targets1.append(targets[k].value) + layer.targets2.append(targets[k + 1].value) + layer.bases.append(basis) + + def _feed_c( + self, + basis1: Literal["X", "Y", "Z"], + basis2: Literal["X", "Y", "Z"], + targets: list[stim.GateTarget], + ): + is_feedback = any(t.is_sweep_bit_target or t.is_measurement_record_target for t in targets) + if is_feedback: + f_layer: FeedbackLayer = self._feed(FeedbackLayer) + for k in range(0, len(targets), 2): + c = targets[k] + t = targets[k + 1] + if t.is_sweep_bit_target or t.is_measurement_record_target: + c, t = t, c + f_layer.bases.append(basis1) + else: + f_layer.bases.append(basis2) + f_layer.controls.append(c) + f_layer.targets.append(t.value) + else: + i_layer: InteractLayer = self._feed(InteractLayer) + for k in range(0, len(targets), 2): + i_layer.bases1.append(basis1) + i_layer.bases2.append(basis2) + i_layer.targets1.append(targets[k].value) + i_layer.targets2.append(targets[k + 1].value) + + @staticmethod + def from_stim_circuit(circuit: stim.Circuit) -> LayerCircuit: + result = LayerCircuit() + for instruction in circuit: + gate_data = stim.gate_data(instruction.name) + if isinstance(instruction, stim.CircuitRepeatBlock): + result.layers.append( + LoopLayer( + body=LayerCircuit.from_stim_circuit(instruction.body_copy()), + repetitions=instruction.repeat_count, + ) + ) + + elif instruction.tag: + result._feed_tag(instruction) + + elif instruction.name == "R": + result._feed_reset("Z", instruction.targets_copy()) + elif instruction.name == "RX": + result._feed_reset("X", instruction.targets_copy()) + elif instruction.name == "RY": + result._feed_reset("Y", instruction.targets_copy()) + + elif instruction.name == "M": + result._feed_m("Z", instruction.targets_copy()) + elif instruction.name == "MX": + result._feed_m("X", instruction.targets_copy()) + elif instruction.name == "MY": + result._feed_m("Y", instruction.targets_copy()) + + elif instruction.name == "MR": + result._feed_m("Z", instruction.targets_copy()) + result._feed_reset("Z", instruction.targets_copy()) + elif instruction.name == "MRX": + result._feed_m("X", instruction.targets_copy()) + result._feed_reset("X", instruction.targets_copy()) + elif instruction.name == "MRY": + result._feed_m("Y", instruction.targets_copy()) + result._feed_reset("Y", instruction.targets_copy()) + + elif instruction.name == "XCX": + result._feed_c("X", "X", instruction.targets_copy()) + elif instruction.name == "XCY": + result._feed_c("X", "Y", instruction.targets_copy()) + elif instruction.name == "XCZ": + result._feed_c("X", "Z", instruction.targets_copy()) + elif instruction.name == "YCX": + result._feed_c("Y", "X", instruction.targets_copy()) + elif instruction.name == "YCY": + result._feed_c("Y", "Y", instruction.targets_copy()) + elif instruction.name == "YCZ": + result._feed_c("Y", "Z", instruction.targets_copy()) + elif instruction.name == "CX": + result._feed_c("Z", "X", instruction.targets_copy()) + elif instruction.name == "CY": + result._feed_c("Z", "Y", instruction.targets_copy()) + elif instruction.name == "CZ": + result._feed_c("Z", "Z", instruction.targets_copy()) + + elif gate_data.is_single_qubit_gate and gate_data.is_unitary: + result._feed_named_rotation_instruction(instruction) + + elif instruction.name == "QUBIT_COORDS": + result._feed_qubit_coords(instruction.targets_copy(), instruction.gate_args_copy()) + elif instruction.name == "SHIFT_COORDS": + result._feed_shift_coords(instruction.gate_args_copy()) + elif instruction.name in ["DETECTOR", "OBSERVABLE_INCLUDE"]: + result._feed(DetObsAnnotationLayer).circuit.append(instruction) + + elif instruction.name in ["ISWAP", "ISWAP_DAG"]: + result._feed_iswap(instruction.targets_copy()) + elif instruction.name == "MPP": + result._feed_mpp(instruction.targets_copy()) + elif instruction.name == "SWAP": + result._feed_swap(instruction.targets_copy()) + elif instruction.name == "CXSWAP": + result._feed_cxswap(instruction.targets_copy()) + elif instruction.name == "SWAPCX": + result._feed_swapcx(instruction.targets_copy()) + + elif instruction.name == "TICK": + result.layers.append(EmptyLayer()) + + elif instruction.name == "SQRT_XX" or instruction.name == "SQRT_XX_DAG": + result._feed_sqrt_pp("X", instruction.targets_copy()) + elif instruction.name == "SQRT_YY" or instruction.name == "SQRT_YY_DAG": + result._feed_sqrt_pp("Y", instruction.targets_copy()) + elif instruction.name == "SQRT_ZZ" or instruction.name == "SQRT_ZZ_DAG": + result._feed_sqrt_pp("Z", instruction.targets_copy()) + elif ( + instruction.name == "DEPOLARIZE1" + or instruction.name == "X_ERROR" + or instruction.name == "Y_ERROR" + or instruction.name == "Z_ERROR" + or instruction.name == "DEPOLARIZE2" + ): + result._feed(NoiseLayer).circuit.append(instruction) + + else: + raise NotImplementedError(f"{instruction=}") + return result + + def __repr__(self) -> str: + result = ["LayerCircuit(layers=["] + for layer in self.layers: + r = repr(layer) + for line in r.split("\n"): + result.append("\n " + line) + result.append(",") + result.append("\n])") + return "".join(result) + + def with_qubit_coords_at_start(self) -> LayerCircuit: + k = len(self.layers) + merged_layer = QubitCoordAnnotationLayer() + rev_layers: list[Layer] = [] + while k > 0: + k -= 1 + layer = self.layers[k] + if isinstance(layer, QubitCoordAnnotationLayer): + intersection = merged_layer.coords.keys() & layer.coords.keys() + if intersection: + raise ValueError( + f"Qubit coords specified twice for qubits {sorted(intersection)}" + ) + merged_layer.coords.update(layer.coords) + elif isinstance(layer, ShiftCoordAnnotationLayer): + merged_layer.offset_by(layer.shift) + rev_layers.append(layer) + elif isinstance(layer, LoopLayer): + if merged_layer.coords: + raise NotImplementedError("Moving qubit coords across a loop.") + rev_layers.append(layer) + else: + rev_layers.append(layer) + rev_layers.append(merged_layer) + return LayerCircuit(layers=rev_layers[::-1]) + + def with_locally_optimized_layers(self) -> LayerCircuit: + """Iterates over the circuit aggregating layer.optimized(second_layer).""" + new_layers: list[Layer] = [] + + def do_layer(layer: Layer | None): + if new_layers: + new_layers[-1:] = new_layers[-1].locally_optimized(layer) + else: + new_layers.append(layer) + while new_layers and (new_layers[-1] is None or new_layers[-1].is_vacuous()): + new_layers.pop() + + for e in self.layers: + for opt in e.locally_optimized(None): + do_layer(opt) + do_layer(None) + return LayerCircuit(layers=new_layers) + + def _resets_at_layer(self, k: int, *, end_resets: set[int]) -> set[int]: + if k >= len(self.layers): + return end_resets + + layer = self.layers[k] + if isinstance(layer, ResetLayer): + return layer.touched() + if isinstance(layer, LoopLayer): + return layer.body._resets_at_layer(0, end_resets=set()) + return set() + + def with_rotations_before_resets_removed( + self, loop_boundary_resets: set[int] | None = None + ) -> LayerCircuit: + all_touched = self.touched() + if loop_boundary_resets is None: + loop_boundary_resets = set() + sets: list[set[int]] = [layer.touched() for layer in self.layers] + sets.append(all_touched) + resets: list[set[int]] = [ + self._resets_at_layer(k, end_resets=all_touched) for k in range(len(self.layers)) + ] + if loop_boundary_resets is None: + resets.append(all_touched) + elif len(resets) == 0: + resets.append(set()) + else: + resets.append(loop_boundary_resets & resets[0]) + new_layers: list[Layer] = [layer.copy() for layer in self.layers] + + for k, layer in enumerate(new_layers): + if isinstance(layer, LoopLayer): + layer.body = layer.body.with_rotations_before_resets_removed( + loop_boundary_resets=self._resets_at_layer(k + 1, end_resets=all_touched) + ) + elif isinstance(layer, RotationLayer): + drops = [] + for q, gate in layer.named_rotations.items(): + if gate != "I": + k2 = k + 1 + while k2 < len(sets): + if q in sets[k2]: + if q in resets[k2]: + drops.append(q) + break + k2 += 1 + for q in drops: + del layer.named_rotations[q] + + return LayerCircuit([layer for layer in new_layers if not layer.is_vacuous()]) + + def with_clearable_rotation_layers_cleared(self) -> LayerCircuit: + """Removes rotation layers where every rotation in the layer can be moved to another layer. + + Each individual rotation can move through intermediate non-rotation layers as long as those + layers don't touch the qubit being rotated. + """ + sets = [layer.touched() for layer in self.layers] + + def scan(qubit: int, start_layer: int, delta: int) -> int | None: + while True: + start_layer += delta + if start_layer < 0 or start_layer >= len(sets): + return None + if ( + isinstance(new_layers[start_layer], RotationLayer) + and not new_layers[start_layer].is_vacuous() + ): + return start_layer + if qubit in sets[start_layer]: + return None + + new_layers = [layer.copy() for layer in self.layers] + cur_layer_index = 0 + while cur_layer_index < len(new_layers): + layer = new_layers[cur_layer_index] + if isinstance(layer, RotationLayer): + rewrites = {} + for q, r in layer.named_rotations.items(): + if r == "I": + continue + new_layer_index = scan(q, cur_layer_index, -1) + if new_layer_index is None: + new_layer_index = scan(q, cur_layer_index, +1) + if new_layer_index is not None: + rewrites[q] = new_layer_index + else: + break + else: + for q, r in layer.named_rotations.items(): + if r == "I": + continue + new_layer_index = rewrites[q] + new_layer: RotationLayer = cast(RotationLayer, new_layers[new_layer_index]) + if new_layer_index > cur_layer_index: + new_layer.prepend_named_rotation(r, q) + else: + new_layer.append_named_rotation(r, q) + if new_layer.named_rotations.get(q) != "I": + sets[new_layer_index].add(q) + elif q in sets[new_layer_index]: + sets[new_layer_index].remove(q) + layer.named_rotations.clear() + sets[cur_layer_index].clear() + elif isinstance(layer, LoopLayer): + layer.body = layer.body.with_clearable_rotation_layers_cleared() + cur_layer_index += 1 + return LayerCircuit([layer for layer in new_layers if not layer.is_vacuous()]) + + def with_rotations_rolled_from_end_of_loop_to_start_of_loop(self) -> LayerCircuit: + """Rewrites loops so that they only have rotations at the start, not the end. + + This is useful for ensuring loops don't redundantly rotate at the loop boundary, + by merging the rotations at the end with the rotations at the start or by + making it clear rotations at the end were not needed because of the + operations coming next. + + For example, this: + + REPEAT 5 { + S 2 3 4 + R 0 1 + ... + M 0 1 + H 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + DETECTOR rec[-1] + } + + will become this: + + REPEAT 5 { + H 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + S 2 3 4 + R 0 1 + ... + M 0 1 + DETECTOR rec[-1] + } + + which later optimization passes can then reduce further. + """ + + new_layers: list[Layer] = [] + for layer in self.layers: + handled = False + if isinstance(layer, LoopLayer): + loop_layers = list(layer.body.layers) + rot_layer_index = len(loop_layers) - 1 + while rot_layer_index > 0: + if isinstance( + loop_layers[rot_layer_index], + (DetObsAnnotationLayer, ShiftCoordAnnotationLayer), + ): + rot_layer_index -= 1 + continue + if isinstance(loop_layers[rot_layer_index], RotationLayer): + break + # Loop didn't end with a rotation layer; give up. + rot_layer_index = 0 + if rot_layer_index > 0: + handled = True + popped = cast(RotationLayer, loop_layers.pop(rot_layer_index)) + loop_layers.insert(0, popped) + + new_layers.append(popped.inverse()) + new_layers.append( + LoopLayer(body=LayerCircuit(loop_layers), repetitions=layer.repetitions) + ) + new_layers.append(popped.copy()) + if not handled: + new_layers.append(layer) + return LayerCircuit([layer for layer in new_layers if not layer.is_vacuous()]) + + def with_rotations_merged_earlier(self) -> LayerCircuit: + sets = [layer.touched() for layer in self.layers] + + def scan(qubit: int, start_layer: int) -> int | None: + while True: + start_layer -= 1 + if start_layer < 0: + return None + l = new_layers[start_layer] + if isinstance(l, RotationLayer) and qubit in l.named_rotations: + return start_layer + if qubit in sets[start_layer]: + return None + + new_layers = [layer.copy() for layer in self.layers] + cur_layer_index = 0 + while cur_layer_index < len(new_layers): + layer = new_layers[cur_layer_index] + if isinstance(layer, RotationLayer): + rewrites = {} + for q, gate in layer.named_rotations.items(): + if gate == "I": + continue + v = scan(q, cur_layer_index) + if v is not None: + rewrites[q] = v + for q, dst in rewrites.items(): + new_layer: RotationLayer = cast(RotationLayer, new_layers[dst]) + new_layer.append_named_rotation(layer.named_rotations.pop(q), q) + sets[cur_layer_index].remove(q) + if new_layer.named_rotations.get(q): + sets[dst].add(q) + elif q in sets[dst]: + sets[dst].remove(q) + elif isinstance(layer, LoopLayer): + layer.body = layer.body.with_rotations_merged_earlier() + cur_layer_index += 1 + return LayerCircuit([layer for layer in new_layers if not layer.is_vacuous()]) + + def with_whole_rotation_layers_slid_earlier(self) -> LayerCircuit: + rev_layers: list[Layer] = [] + cur_rot_layer: RotationLayer | None = None + cur_rot_touched: set[int] | None = None + for layer in self.layers[::-1]: + if cur_rot_layer is not None and not layer.touched().isdisjoint(cur_rot_touched): + rev_layers.append(cur_rot_layer) + cur_rot_layer = None + cur_rot_touched = None + if isinstance(layer, RotationLayer): + layer = layer.copy() + if cur_rot_layer is not None: + layer.named_rotations.update(cur_rot_layer.named_rotations) + cur_rot_layer = layer + cur_rot_touched = cur_rot_layer.touched() + else: + rev_layers.append(layer) + if cur_rot_layer is not None: + rev_layers.append(cur_rot_layer) + return LayerCircuit(rev_layers[::-1]) + + def with_ejected_loop_iterations(self) -> LayerCircuit: + """Partially unrolls loops, placing one iteration before and one iteration after. + + This is useful for ensuring the transition into and out of a loop is optimized correctly. + For example, if a circuit begins with a transversal initialization of data qubits and then + immediately starts a memory loop, the resets from the data initialization should be merged + into the same layer as the resets from the measurement initialization at the beginning of + the loop. But the reset-merging optimization might not see that this is possible across the + loop boundary. Ejecting an iteration fixes this issue. + + For example, this method would turn this circuit fragment: + + REPEAT 100 { + X 0 + MR 0 + } + + into this circuit fragment: + + X 0 + MR 0 + REPEAT 98 { + X 0 + MR 0 + } + X 0 + MR 0 + """ + new_layers: list[Layer] = [] + for layer in self.layers: + if isinstance(layer, LoopLayer): + if layer.repetitions == 0: + pass + elif layer.repetitions == 1: + new_layers.extend(layer.body.layers) + elif layer.repetitions == 2: + new_layers.extend(layer.body.layers) + new_layers.extend(layer.body.layers) + else: + new_layers.extend(layer.body.layers) + new_layers.append( + LoopLayer(body=layer.body.copy(), repetitions=layer.repetitions - 2) + ) + new_layers.extend(layer.body.layers) + assert layer.repetitions > 2 + else: + new_layers.append(layer) + return LayerCircuit(new_layers) + + def without_empty_layers(self) -> LayerCircuit: + """Removes empty layers from the circuit. + + Empty layers are sometimes created as a byproduct of certain optimizations, or may have been + present in the original circuit. Usually they are unwanted, and this method removes them. + """ + new_layers: list[Layer] = [] + for layer in self.layers: + if isinstance(layer, EmptyLayer): + pass + elif isinstance(layer, LoopLayer): + new_layers.append(LoopLayer(layer.body.without_empty_layers(), layer.repetitions)) + else: + new_layers.append(layer) + return LayerCircuit(new_layers) + + def with_cleaned_up_loop_iterations(self) -> LayerCircuit: + """Attempts to roll up partially unrolled loops. + + Checks if the instructions before a loop correspond to the instruction inside a loop. If so, + removes the matching instructions beforehand and increases the iteration count by 1. Same + for instructions after the loop. + + This essentially undoes the effect of `with_ejected_loop_iterations`. A common pattern is + to do `with_ejected_loop_iterations`, then an optimization, then + `with_cleaned_up_loop_iterations`. This gives the optimization the chance to optimize across + a loop boundary, but cleans up after itself if no optimization occurs. + + In some cases this method is useful because of circuit generation code being overly cautious + about how quickly loop invariants are established, and so emitting the first iteration of a + loop in a special way. If it happens to be identical, despite the different code path that + produced it, this method will roll it into the rest of the loop. + + For example, this method would turn this circuit fragment: + + X 0 + MR 0 + REPEAT 98 { + X 0 + MR 0 + } + X 0 + MR 0 + + into this circuit fragment: + + REPEAT 100 { + X 0 + MR 0 + } + """ + new_layers = list(self.without_empty_layers().layers) + k = 0 + while k < len(new_layers): + cur_layer = new_layers[k] + if isinstance(cur_layer, LoopLayer): + body_layers = cur_layer.body.layers + reps = cur_layer.repetitions + while k >= len(body_layers) and new_layers[k - len(body_layers) : k] == body_layers: + new_layers[k - len(body_layers) : k] = [] + k -= len(body_layers) + reps += 1 + while ( + k + len(body_layers) < len(new_layers) + and new_layers[k + 1 : k + 1 + len(body_layers)] == body_layers + ): + new_layers[k + 1 : k + 1 + len(body_layers)] = [] + reps += 1 + new_layers[k] = LoopLayer(LayerCircuit(body_layers), reps) + k += 1 + return LayerCircuit(new_layers) + + def with_locally_merged_measure_layers(self) -> LayerCircuit: + """Merges measurement layers together, despite intervening annotation layers. + + For example, this method would turn this circuit fragment: + + M 0 + DETECTOR(0, 0) rec[-1] + OBSERVABLE_INCLUDE(5) rec[-1] + SHIFT_COORDS(0, 1) + M 1 + DETECTOR(0, 0) rec[-1] + + into this circuit fragment: + + M 0 1 + DETECTOR(0, 0) rec[-2] + OBSERVABLE_INCLUDE(5) rec[-2] + SHIFT_COORDS(0, 1) + DETECTOR(0, 0) rec[-1] + """ + new_layers: list[Layer] = [] + k = 0 + while k < len(self.layers): + cur_layer = self.layers[k] + if isinstance(cur_layer, MeasureLayer): + m1: MeasureLayer = cur_layer + k2 = k + 1 + while k2 < len(self.layers) and isinstance( + self.layers[k2], (DetObsAnnotationLayer, ShiftCoordAnnotationLayer) + ): + k2 += 1 + if k2 < len(self.layers) and isinstance(self.layers[k2], MeasureLayer): + m2: MeasureLayer = cast(MeasureLayer, self.layers[k2]) + if set(m1.targets).isdisjoint(set(m2.targets)): + new_layers.append( + MeasureLayer(targets=m1.targets + m2.targets, bases=m1.bases + m2.bases) + ) + for k3 in range(k + 1, k2): + l3: DetObsAnnotationLayer | ShiftCoordAnnotationLayer + l3 = cast(Any, self.layers[k3]) + new_layers.append(l3.with_rec_targets_shifted_by(-len(m2.targets))) + k = k2 + 1 + continue + new_layers.append(self.layers[k].copy()) + k += 1 + return LayerCircuit(new_layers) + + def with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type( + self, layer_types: type | tuple[type, ...] + ) -> LayerCircuit: + new_layers = list(self.layers) + k = 0 + while k < len(new_layers): + if isinstance(new_layers[k], layer_types): + touched = new_layers[k].touched() + k_prev = k + while k_prev > 0 and new_layers[k_prev - 1].touched().isdisjoint(touched): + k_prev -= 1 + if k_prev != k and type(new_layers[k_prev]) == type(new_layers[k]): + (new_layer,) = ( + e + for e in new_layers[k_prev].locally_optimized(new_layers[k]) + if e is not None + ) + del new_layers[k] + new_layers[k_prev] = new_layer + break + k += 1 + return LayerCircuit(new_layers) + + def with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer( + self, layer_types: type | tuple[type, ...] + ) -> LayerCircuit: + new_layers = list(self.layers) + k = 0 + while k < len(new_layers): + if isinstance(new_layers[k], layer_types): + touched = new_layers[k].touched() + k_prev = k + while k_prev > 0 and new_layers[k_prev - 1].touched().isdisjoint(touched): + k_prev -= 1 + while k_prev < k and type(new_layers[k_prev]) != type(new_layers[k]): + k_prev += 1 + if k_prev != k: + (new_layer,) = ( + e + for e in new_layers[k_prev].locally_optimized(new_layers[k]) + if e is not None + ) + del new_layers[k] + new_layers[k_prev] = new_layer + continue + k += 1 + return LayerCircuit(new_layers) + + def with_irrelevant_tail_layers_removed(self) -> LayerCircuit: + irrelevant_layer_types_at_end = ( + ResetLayer, + InteractLayer, + FeedbackLayer, + RotationLayer, + SwapLayer, + ISwapLayer, + InteractSwapLayer, + EmptyLayer, + ) + tail = [] + result = list(self.layers) + while True: + while len(result) > 0 and isinstance(result[-1], irrelevant_layer_types_at_end): + result.pop() + if len(result) > 0 and isinstance(result[-1], DetObsAnnotationLayer): + tail.append(result.pop()) + else: + break + result.extend(tail) + return LayerCircuit(result) + + def to_stim_circuit(self) -> stim.Circuit: + """Compiles the layer circuit into a stim circuit and returns it.""" + circuit = stim.Circuit() + tick_coming = False + for layer in self.layers: + if tick_coming and layer.requires_tick_before(): + circuit.append("TICK") + tick_coming = False + layer.append_into_stim_circuit(circuit) + tick_coming |= layer.implies_eventual_tick_after() + return circuit diff --git a/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py b/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py new file mode 100644 index 00000000..bcac224d --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py @@ -0,0 +1,794 @@ +import stim + +from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer_circuit import LayerCircuit +from stimflow._layers._reset_layer import ResetLayer + + +def test_with_squashed_rotations(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + S 0 1 2 3 + TICK + CZ 1 2 + TICK + H 0 3 + TICK + CZ 1 2 + TICK + S 0 1 2 3 + """ + ) + ) + .with_clearable_rotation_layers_cleared() + .to_stim_circuit() + == stim.Circuit( + """ + C_XNYZ 0 3 + S 1 2 + TICK + CZ 1 2 + TICK + CZ 1 2 + TICK + S 0 1 2 3 + """ + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + S 0 1 2 3 + TICK + CZ 0 2 + TICK + H 0 3 + TICK + CZ 1 2 + TICK + S 0 1 2 3 + """ + ) + ) + .with_clearable_rotation_layers_cleared() + .to_stim_circuit() + == stim.Circuit( + """ + C_XNYZ 3 + S 0 1 2 + TICK + CZ 0 2 + TICK + CZ 1 2 + TICK + C_ZYX 0 + S 1 2 3 + """ + ) + ) + + +def test_with_rotations_before_resets_removed(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + H 0 1 2 3 + TICK + R 0 1 + """ + ) + ) + .with_rotations_before_resets_removed() + .to_stim_circuit() + == stim.Circuit( + """ + H 2 3 + TICK + R 0 1 + """ + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + H 0 1 2 3 + TICK + REPEAT 100 { + R 0 1 + TICK + H 0 1 2 3 + TICK + } + R 1 2 + TICK + """ + ) + ) + .with_rotations_before_resets_removed() + .to_stim_circuit() + == stim.Circuit( + """ + H 2 3 + TICK + REPEAT 100 { + R 0 1 + TICK + H 0 2 3 + TICK + } + R 1 2 + """ + ) + ) + + +def test_with_rotations_merged_earlier(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + S 0 1 2 3 + TICK + CZ 1 2 3 4 + TICK + H 0 1 2 3 + TICK + CZ 1 2 + TICK + S 0 1 2 3 + """ + ) + ) + .with_rotations_merged_earlier() + .to_stim_circuit() + == stim.Circuit( + """ + S 1 2 3 + SQRT_X_DAG 0 + TICK + CZ 1 2 3 4 + TICK + C_ZYX 3 + H 1 2 + TICK + CZ 1 2 + TICK + S 1 2 + """ + ) + ) + + +def test_with_qubit_coords_at_start(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + QUBIT_COORDS(2, 3) 0 + SHIFT_COORDS(0, 0, 100) + QUBIT_COORDS(5, 7) 1 + SHIFT_COORDS(0, 200) + QUBIT_COORDS(11, 13) 2 + SHIFT_COORDS(300) + R 0 1 + TICK + QUBIT_COORDS(17) 3 + H 3 + TICK + QUBIT_COORDS(19, 23, 29) 4 + REPEAT 10 { + M 0 1 2 3 + DETECTOR rec[-1] + } + """ + ) + ) + .with_qubit_coords_at_start() + .to_stim_circuit() + == stim.Circuit( + """ + QUBIT_COORDS(2, 3) 0 + QUBIT_COORDS(5, 7) 1 + QUBIT_COORDS(11, 213) 2 + QUBIT_COORDS(317) 3 + QUBIT_COORDS(319, 223, 129) 4 + SHIFT_COORDS(0, 0, 100) + SHIFT_COORDS(0, 200) + SHIFT_COORDS(300) + R 0 1 + TICK + H 3 + TICK + REPEAT 10 { + M 0 1 2 3 + DETECTOR rec[-1] + TICK + } + """ + ) + ) + + +def test_merge_shift_coords(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + SHIFT_COORDS(300) + SHIFT_COORDS(0, 0, 100) + SHIFT_COORDS(0, 200) + """ + ) + ) + .with_locally_optimized_layers() + .to_stim_circuit() + == stim.Circuit( + """ + SHIFT_COORDS(300, 200, 100) + """ + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + SHIFT_COORDS(300) + TICK + SHIFT_COORDS(0, 0, 100) + TICK + SHIFT_COORDS(0, 200) + TICK + """ + ) + ) + .with_locally_optimized_layers() + .to_stim_circuit() + == stim.Circuit( + """ + SHIFT_COORDS(300, 200, 100) + """ + ) + ) + + +def test_merge_resets_and_measurements(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + RX 0 1 + TICK + RY 2 3 + """ + ) + ) + .with_locally_optimized_layers() + .to_stim_circuit() + == stim.Circuit( + """ + RX 0 1 + RY 2 3 + """ + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + RX 0 1 + TICK + RY 1 2 3 + """ + ) + ) + .with_locally_optimized_layers() + .to_stim_circuit() + == stim.Circuit( + """ + RX 0 + RY 1 2 3 + """ + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + MX 0 1 + TICK + MY 2 3 + """ + ) + ) + .with_locally_optimized_layers() + .to_stim_circuit() + == stim.Circuit( + """ + MX 0 1 + MY 2 3 + """ + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + MX 0 1 + TICK + MY 1 2 3 + """ + ) + ) + .with_locally_optimized_layers() + .to_stim_circuit() + == stim.Circuit( + """ + MX 0 1 + TICK + MY 1 2 3 + """ + ) + ) + + +def test_swap_cancellation(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + SWAP 0 1 2 3 4 5 + TICK + SWAP 2 3 4 6 7 8 + """ + ) + ).with_locally_optimized_layers() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + SWAP 0 1 4 5 7 8 + TICK + SWAP 4 6 + """ + ) + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + SWAP 0 1 2 3 4 5 + TICK + SWAP 2 3 + """ + ) + ).with_locally_optimized_layers() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + SWAP 0 1 4 5 + """ + ) + ) + ) + + +def test_with_rotation_layers_moved_earlier(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + CX 0 1 + TICK + SWAP 2 3 + TICK + H 1 4 5 6 + """ + ) + ).with_whole_rotation_layers_slid_earlier() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + CX 0 1 + TICK + H 1 4 5 6 + TICK + SWAP 2 3 + """ + ) + ) + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + CX 0 1 + TICK + S 7 + TICK + SWAP 2 3 + TICK + H 1 4 5 6 + """ + ) + ).with_whole_rotation_layers_slid_earlier() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + CX 0 1 + TICK + S 7 + H 1 4 5 6 + TICK + SWAP 2 3 + """ + ) + ) + ) + + +def test_with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + + R 0 1 + TICK + + CX 0 1 + TICK + + CX 1 0 + TICK + + CX 0 1 + TICK + + M 5 + DETECTOR rec[-1] + TICK + + R 2 3 + TICK + + CX 2 3 + TICK + + CX 3 4 + """ + ) + ) + .with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type(ResetLayer) + .with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(InteractLayer) + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + + R 0 1 2 3 + TICK + + CX 0 1 2 3 + TICK + + CX 1 0 3 4 + TICK + + CX 0 1 + TICK + + M 5 + DETECTOR rec[-1] + """ + ) + ) + ) + + +def test_with_cleaned_up_loop_iterations(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + """ + ) + ).with_cleaned_up_loop_iterations() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + REPEAT 6 { + R 0 1 + TICK + S 2 + TICK + } + """ + ) + ).without_empty_layers() + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + H 1 + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + """ + ) + ).with_cleaned_up_loop_iterations() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + H 1 + + REPEAT 6 { + R 0 1 + TICK + S 2 + TICK + } + """ + ) + ).without_empty_layers() + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + S 2 + TICK + + H 1 + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + """ + ) + ).with_cleaned_up_loop_iterations() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + S 2 + TICK + + H 1 + + REPEAT 7 { + R 0 1 + TICK + S 2 + TICK + } + """ + ) + ).without_empty_layers() + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + S 2 + TICK + + H 1 + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + + R 0 1 + TICK + S 2 + TICK + """ + ) + ).with_cleaned_up_loop_iterations() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + S 2 + TICK + + H 1 + + REPEAT 8 { + R 0 1 + TICK + S 2 + TICK + } + """ + ) + ).without_empty_layers() + ) + + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + S 2 + TICK + + H 1 + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + H 0 + TICK + + R 0 1 + TICK + S 2 + TICK + """ + ) + ).with_cleaned_up_loop_iterations() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 + TICK + S 2 + TICK + + H 1 + + REPEAT 9 { + R 0 1 + TICK + S 2 + TICK + } + + H 0 + TICK + + R 0 1 + TICK + S 2 + TICK + """ + ) + ).without_empty_layers() + ) + + +def test_with_locally_merged_measure_layers(): + assert ( + LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 2 + TICK + CX 0 1 + TICK + CX 2 1 + TICK + M 1 + DETECTOR rec[-1] + TICK + M 0 2 + DETECTOR rec[-1] rec[-2] rec[-3] + """ + ) + ) + .with_locally_merged_measure_layers() + .with_locally_optimized_layers() + == LayerCircuit.from_stim_circuit( + stim.Circuit( + """ + R 0 1 2 + TICK + CX 0 1 + TICK + CX 2 1 + TICK + M 1 0 2 + DETECTOR rec[-3] + DETECTOR rec[-1] rec[-2] rec[-3] + """ + ) + ) + ) diff --git a/glue/stimflow/src/stimflow/_layers/_loop_layer.py b/glue/stimflow/src/stimflow/_layers/_loop_layer.py new file mode 100644 index 00000000..03c25ec4 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_loop_layer.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import dataclasses +from typing import TYPE_CHECKING + +import stim + +from stimflow._layers._layer import Layer + +if TYPE_CHECKING: + from stimflow._layers._layer_circuit import LayerCircuit + + +@dataclasses.dataclass +class LoopLayer(Layer): + body: LayerCircuit + repetitions: int + + def copy(self) -> LoopLayer: + return LoopLayer(body=self.body.copy(), repetitions=self.repetitions) + + def touched(self) -> set[int]: + return self.body.touched() + + def to_z_basis(self) -> list[Layer]: + return [LoopLayer(body=self.body.to_z_basis(), repetitions=self.repetitions)] + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + optimized = LoopLayer( + body=self.body.with_locally_optimized_layers(), repetitions=self.repetitions + ) + return [optimized, next_layer] + + def implies_eventual_tick_after(self) -> bool: + return False + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + body = self.body.to_stim_circuit() + body.append("TICK") + out.append(stim.CircuitRepeatBlock(repeat_count=self.repetitions, body=body)) diff --git a/glue/stimflow/src/stimflow/_layers/_measure_layer.py b/glue/stimflow/src/stimflow/_layers/_measure_layer.py new file mode 100644 index 00000000..620c01b2 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_measure_layer.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer +from stimflow._layers._rotation_layer import RotationLayer + + +@dataclasses.dataclass +class MeasureLayer(Layer): + """A layer of single qubit Pauli basis measurement operations.""" + + targets: list[int] = dataclasses.field(default_factory=list) + bases: list[str] = dataclasses.field(default_factory=list) + + def copy(self) -> MeasureLayer: + return MeasureLayer(targets=list(self.targets), bases=list(self.bases)) + + def touched(self) -> set[int]: + return set(self.targets) + + def to_z_basis(self) -> list[Layer]: + rot = RotationLayer( + { + q: "I" if b == "Z" else "H" if b == "X" else "H_YZ" + for q, b in zip(self.targets, self.bases) + } + ) + return [ + rot, + MeasureLayer(targets=list(self.targets), bases=["Z"] * len(self.targets)), + rot.copy(), + ] + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + for t, b in zip(self.targets, self.bases): + out.append("M" + b, [t]) + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + if isinstance(next_layer, MeasureLayer) and set(self.targets).isdisjoint( + next_layer.targets + ): + return [ + MeasureLayer( + targets=self.targets + next_layer.targets, bases=self.bases + next_layer.bases + ) + ] + if isinstance(next_layer, RotationLayer) and set(self.targets).isdisjoint( + next_layer.named_rotations.keys() + ): + return [next_layer, self] + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_mpp_layer.py b/glue/stimflow/src/stimflow/_layers/_mpp_layer.py new file mode 100644 index 00000000..8ee4ff59 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_mpp_layer.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class MppLayer(Layer): + targets: list[list[stim.GateTarget]] = dataclasses.field(default_factory=list) + + def copy(self) -> MppLayer: + return MppLayer(targets=[list(e) for e in self.targets]) + + def touched(self) -> set[int]: + return {t.value for mpp in self.targets for t in mpp} + + def to_z_basis(self) -> list[Layer]: + return [self] + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + flat_targets = [] + for group in self.targets: + for t in group: + flat_targets.append(t) + flat_targets.append(stim.target_combiner()) + flat_targets.pop() + out.append("MPP", flat_targets) diff --git a/glue/stimflow/src/stimflow/_layers/_noise_layer.py b/glue/stimflow/src/stimflow/_layers/_noise_layer.py new file mode 100644 index 00000000..ad2b6a90 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_noise_layer.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class NoiseLayer(Layer): + """A layer of noise operations.""" + + circuit: stim.Circuit = dataclasses.field(default_factory=stim.Circuit) + + def copy(self) -> NoiseLayer: + return NoiseLayer(circuit=self.circuit.copy()) + + def touched(self) -> set[int]: + return { + target.qubit_value + for instruction in self.circuit + for target in instruction.targets_copy() + } + + def requires_tick_before(self) -> bool: + return False + + def implies_eventual_tick_after(self) -> bool: + return False + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + out += self.circuit diff --git a/glue/stimflow/src/stimflow/_layers/_qubit_coord_annotation_layer.py b/glue/stimflow/src/stimflow/_layers/_qubit_coord_annotation_layer.py new file mode 100644 index 00000000..44e1de14 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_qubit_coord_annotation_layer.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import dataclasses +from collections.abc import Iterable + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class QubitCoordAnnotationLayer(Layer): + coords: dict[int, list[float]] = dataclasses.field(default_factory=dict) + + def offset_by(self, args: Iterable[float]): + for index, offset in enumerate(args): + if offset: + for qubit_coords in self.coords.values(): + if index < len(qubit_coords): + qubit_coords[index] += offset + + def copy(self) -> Layer: + return QubitCoordAnnotationLayer(coords=dict(self.coords)) + + def touched(self) -> set[int]: + return set() + + def requires_tick_before(self) -> bool: + return False + + def implies_eventual_tick_after(self) -> bool: + return False + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + for q in sorted(self.coords.keys()): + out.append("QUBIT_COORDS", [q], self.coords[q]) diff --git a/glue/stimflow/src/stimflow/_layers/_reset_layer.py b/glue/stimflow/src/stimflow/_layers/_reset_layer.py new file mode 100644 index 00000000..f3ca1c54 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_reset_layer.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import dataclasses +from typing import Literal + +import stim + +from stimflow._layers._layer import Layer +from stimflow._layers._rotation_layer import RotationLayer + + +@dataclasses.dataclass +class ResetLayer(Layer): + """A layer of reset gates.""" + + targets: dict[int, Literal["X", "Y", "Z"]] = dataclasses.field(default_factory=dict) + + def copy(self) -> ResetLayer: + return ResetLayer(targets=dict(self.targets)) + + def touched(self) -> set[int]: + return set(self.targets.keys()) + + def to_z_basis(self) -> list[Layer]: + return [ + ResetLayer(targets={q: "Z" for q in self.targets.keys()}), + RotationLayer( + { + q: "I" if b == "Z" else "H" if b == "X" else "H_YZ" + for q, b in self.targets.items() + } + ), + ] + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + basis: Literal["X", "Y", "Z"] + outs: dict[Literal["X", "Y", "Z"], list[int]] = {"X": [], "Y": [], "Z": []} + for target, basis in self.targets.items(): + outs[basis].append(target) + for basis, vs in outs.items(): + if vs: + out.append("R" + basis, vs) + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + if isinstance(next_layer, ResetLayer): + return [ + ResetLayer( + targets={t: b for layer in [self, next_layer] for t, b in layer.targets.items()} + ) + ] + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_rotation_layer.py b/glue/stimflow/src/stimflow/_layers/_rotation_layer.py new file mode 100644 index 00000000..7c1b247a --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_rotation_layer.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._data import ( + single_qubit_clifford_inverse_table, + single_qubit_clifford_multiplication_table, +) +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class RotationLayer(Layer): + """A layer of single qubit Clifford rotation gates.""" + + named_rotations: dict[int, str] = dataclasses.field(default_factory=dict) + + def touched(self) -> set[int]: + return {k for k, v in self.named_rotations.items() if v != "I"} + + def copy(self) -> RotationLayer: + return RotationLayer(dict(self.named_rotations)) + + def inverse(self) -> RotationLayer: + t = single_qubit_clifford_inverse_table() + return RotationLayer({q: t[r] for q, r in self.named_rotations.items()}) + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + gate2targets: dict[str, list[int]] = {} + for key, val in self.named_rotations.items(): + gate2targets.setdefault(val, []).append(key) + + for gate, qs in sorted(gate2targets.items()): + if gate != "I": + qs = sorted(qs) + if "*" in gate: + after, before = gate.split("*") + out.append(before, qs) + out.append(after, qs) + else: + out.append(gate, qs) + + def prepend_named_rotation(self, name: str, target: int): + m = single_qubit_clifford_multiplication_table() + cur = self.named_rotations.get(target, "I") + new_val = m[(cur, name)] + if new_val == "I": + self.named_rotations.pop(target, None) + else: + self.named_rotations[target] = new_val + + def append_named_rotation(self, name: str, target: int): + m = single_qubit_clifford_multiplication_table() + cur = self.named_rotations.get(target, "I") + new_val = m[(name, cur)] + if new_val == "I": + self.named_rotations.pop(target, None) + else: + self.named_rotations[target] = new_val + + def is_vacuous(self) -> bool: + return not any(self.named_rotations.values()) + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + from stimflow._layers._det_obs_annotation_layer import DetObsAnnotationLayer + from stimflow._layers._feedback_layer import FeedbackLayer + from stimflow._layers._reset_layer import ResetLayer + from stimflow._layers._shift_coord_annotation_layer import ShiftCoordAnnotationLayer + + if isinstance(next_layer, (DetObsAnnotationLayer, ShiftCoordAnnotationLayer)): + return [next_layer, self] + if isinstance(next_layer, FeedbackLayer): + return [next_layer.before(self), self] + if isinstance(next_layer, ResetLayer): + trimmed = self.copy() + for t in next_layer.targets.keys(): + trimmed.named_rotations.pop(t, None) + if trimmed.named_rotations: + return [trimmed, next_layer] + else: + return [next_layer] + if isinstance(next_layer, RotationLayer): + result = self.copy() + for q, r in next_layer.named_rotations.items(): + result.append_named_rotation(r, q) + return [result] + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_rotation_layer_test.py b/glue/stimflow/src/stimflow/_layers/_rotation_layer_test.py new file mode 100644 index 00000000..e088a3c0 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_rotation_layer_test.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import stim + +import stimflow + + +def test_fuses_rotations(): + layer = stimflow.RotationLayer() + layer.append_named_rotation("H", 0) + layer.append_named_rotation("H_NXZ", 0) + assert layer.named_rotations[0] == "Y" + + +def test_output(): + layer = stimflow.RotationLayer() + + layer.append_named_rotation("H", 0) + layer.append_named_rotation("C_XYZ", 0) + layer.append_named_rotation("S", 0) + + layer.prepend_named_rotation("H", 1) + layer.prepend_named_rotation("C_ZYX", 1) + layer.prepend_named_rotation("S_DAG", 1) + + circuit = stim.Circuit() + layer.append_into_stim_circuit(circuit) + assert circuit == stim.Circuit( + """ + C_NZYX 1 + C_XYNZ 0 + """ + ) diff --git a/glue/stimflow/src/stimflow/_layers/_shift_coord_annotation_layer.py b/glue/stimflow/src/stimflow/_layers/_shift_coord_annotation_layer.py new file mode 100644 index 00000000..09a7dac1 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_shift_coord_annotation_layer.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import dataclasses +from collections.abc import Iterable + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class ShiftCoordAnnotationLayer(Layer): + shift: list[float] = dataclasses.field(default_factory=list) + + def offset_by(self, args: Iterable[float]): + for k, arg in enumerate(args): + if k >= len(self.shift): + self.shift.append(arg) + else: + self.shift[k] += arg + + def copy(self) -> ShiftCoordAnnotationLayer: + return ShiftCoordAnnotationLayer(shift=self.shift) + + def touched(self) -> set[int]: + return set() + + def requires_tick_before(self) -> bool: + return False + + def implies_eventual_tick_after(self) -> bool: + return False + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + out.append("SHIFT_COORDS", [], self.shift) + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + if isinstance(next_layer, ShiftCoordAnnotationLayer): + result = self.copy() + result.offset_by(next_layer.shift) + return [result] + return [self, next_layer] + + def with_rec_targets_shifted_by(self, shift: int) -> ShiftCoordAnnotationLayer: + return self.copy() diff --git a/glue/stimflow/src/stimflow/_layers/_sqrt_pp_layer.py b/glue/stimflow/src/stimflow/_layers/_sqrt_pp_layer.py new file mode 100644 index 00000000..854d9c75 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_sqrt_pp_layer.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import collections +import dataclasses + +import stim + +from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer import Layer +from stimflow._layers._rotation_layer import RotationLayer + + +@dataclasses.dataclass +class SqrtPPLayer(Layer): + targets1: list[int] = dataclasses.field(default_factory=list) + targets2: list[int] = dataclasses.field(default_factory=list) + bases: list[str] = dataclasses.field(default_factory=list) + + def touched(self) -> set[int]: + return set(self.targets1 + self.targets2) + + def copy(self) -> SqrtPPLayer: + return SqrtPPLayer( + targets1=list(self.targets1), targets2=list(self.targets2), bases=list(self.bases) + ) + + def to_z_basis(self) -> list[Layer]: + interact = InteractLayer() + rot = RotationLayer() + for q1, q2, b in zip(self.targets1, self.targets2, self.bases): + interact.targets1.append(q1) + interact.targets2.append(q2) + interact.bases1.append(b) + interact.bases1.append(b) + if b == "X": + r = "SQRT_X" + elif b == "Y": + r = "SQRT_Y" + elif b == "Z": + r = "S" + else: + raise NotImplementedError(f"{b=}") + rot.append_named_rotation(r, q1) + rot.append_named_rotation(r, q2) + + return [rot, *interact.to_z_basis()] + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + groups = collections.defaultdict(list) + for q1, q2, b in zip(self.targets1, self.targets2, self.bases): + gate = f"SQRT_{b}{b}" + if q2 < q1: + q1, q2 = q2, q1 + groups[gate].append((q1, q2)) + for gate in sorted(groups.keys()): + for pair in sorted(groups[gate]): + out.append(gate, pair) diff --git a/glue/stimflow/src/stimflow/_layers/_swap_layer.py b/glue/stimflow/src/stimflow/_layers/_swap_layer.py new file mode 100644 index 00000000..0c6ca132 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_swap_layer.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._det_obs_annotation_layer import DetObsAnnotationLayer +from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer import Layer +from stimflow._layers._shift_coord_annotation_layer import ShiftCoordAnnotationLayer + + +@dataclasses.dataclass +class SwapLayer(Layer): + """A layer of swap gates.""" + + targets1: list[int] = dataclasses.field(default_factory=list) + targets2: list[int] = dataclasses.field(default_factory=list) + + def touched(self) -> set[int]: + return set(self.targets1 + self.targets2) + + def to_swap_dict(self) -> dict[int, int]: + d = {} + for a, b in zip(self.targets1, self.targets2): + d[a] = b + d[b] = a + return d + + def copy(self) -> SwapLayer: + return SwapLayer(targets1=list(self.targets1), targets2=list(self.targets2)) + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + pairs = [] + for k in range(len(self.targets1)): + t1 = self.targets1[k] + t2 = self.targets2[k] + t1, t2 = sorted([t1, t2]) + pairs.append((t1, t2)) + for pair in sorted(pairs): + out.append("SWAP", pair) + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + if isinstance(next_layer, InteractLayer): + from stimflow._layers._interact_swap_layer import InteractSwapLayer + + pairs1 = {frozenset([a, b]) for a, b in zip(self.targets1, self.targets2)} + pairs2 = {frozenset([a, b]) for a, b in zip(next_layer.targets1, next_layer.targets2)} + if pairs1 == pairs2: + i = next_layer.copy() + i.targets1, i.targets2 = i.targets2, i.targets1 + return [InteractSwapLayer(i_layer=i)] + if isinstance(next_layer, (ShiftCoordAnnotationLayer, DetObsAnnotationLayer)): + return [next_layer, self] + if isinstance(next_layer, SwapLayer): + total_swaps = self.to_swap_dict() + leftover_swaps = SwapLayer() + for a, b in zip(next_layer.targets1, next_layer.targets2): + a2 = total_swaps.get(a) + b2 = total_swaps.get(b) + if a2 is None and b2 is None: + total_swaps[a] = b + total_swaps[b] = a + elif a2 == b and b2 == a: + del total_swaps[a] + del total_swaps[b] + else: + leftover_swaps.targets1.append(a) + leftover_swaps.targets2.append(b) + result: list[Layer | None] = [] + if total_swaps: + new_layer = SwapLayer() + for k, v in total_swaps.items(): + if k < v: + new_layer.targets1.append(k) + new_layer.targets2.append(v) + result.append(new_layer) + if leftover_swaps.targets1: + result.append(leftover_swaps) + return result + return [self, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_tag_layer.py b/glue/stimflow/src/stimflow/_layers/_tag_layer.py new file mode 100644 index 00000000..d6621616 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_tag_layer.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class TagLayer(Layer): + circuit: stim.Circuit = dataclasses.field(default_factory=stim.Circuit) + + def copy(self) -> TagLayer: + return TagLayer(circuit=self.circuit) + + def touched(self) -> set[int]: # set of qubit touched by it + tagged_gate_targets = self.circuit[0].target_groups()[0] + return {gate_target.qubit_value for gate_target in tagged_gate_targets} + + def to_z_basis(self) -> list[Layer]: + return [self] + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + return [self, next_layer] + + def is_vacuous(self) -> bool: + return False + + def requires_tick_before(self) -> bool: + return True + + def implies_eventual_tick_after(self) -> bool: + return True + + def append_into_stim_circuit(self, out: stim.Circuit) -> None: + out += self.circuit diff --git a/glue/stimflow/src/stimflow/_layers/_tag_layer_test.py b/glue/stimflow/src/stimflow/_layers/_tag_layer_test.py new file mode 100644 index 00000000..decfa8b6 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_tag_layer_test.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import stim + +import stimflow + + +def test_survives_transpile(): + circuit = stim.Circuit( + """ + RX 0 + TICK + CX 0 1 + TICK + CX[test] 0 2 + TICK + MRX 0 + SQRT_X[test2] 1 + TICK + CX 0 1 + TICK + CX 0 2 + TICK + MRX 0 + DETECTOR rec[-1] rec[-2] + """ + ) + circuit = stimflow.transpile_to_z_basis_interaction_circuit(circuit) + assert circuit == stim.Circuit( + """ + R 0 + TICK + H 0 1 + TICK + CZ 0 1 + TICK + CX[test] 0 2 + TICK + H 0 1 + TICK + M 0 + TICK + R 0 + TICK + SQRT_X[test2] 1 + TICK + H 0 1 2 + TICK + CZ 0 1 + TICK + CZ 0 2 + TICK + H 0 1 2 + TICK + M 0 + DETECTOR rec[-1] rec[-2] + """ + ) diff --git a/glue/stimflow/src/stimflow/_layers/_transpile.py b/glue/stimflow/src/stimflow/_layers/_transpile.py new file mode 100644 index 00000000..50c7d8c6 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_transpile.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import stim + +from stimflow._layers._layer_circuit import LayerCircuit + + +def transpile_to_z_basis_interaction_circuit( + circuit: stim.Circuit, *, is_entire_circuit: bool = True +) -> stim.Circuit: + """Converts to a circuit using CZ, iSWAP, and MZZ as appropriate. + + This method mostly focuses on inserting single qubit rotations to convert + interactions into their Z basis variant. It also does some optimizations + that remove redundant rotations which would tend to be introduced by this + process. + """ + c = LayerCircuit.from_stim_circuit(circuit) + c = c.with_qubit_coords_at_start() + c = c.with_locally_optimized_layers() + c = c.with_ejected_loop_iterations() + c = c.with_locally_merged_measure_layers() + c = c.with_cleaned_up_loop_iterations() + c = c.to_z_basis() + c = c.with_rotations_rolled_from_end_of_loop_to_start_of_loop() + c = c.with_locally_optimized_layers() + c = c.with_clearable_rotation_layers_cleared() + c = c.with_rotations_merged_earlier() + c = c.with_rotations_before_resets_removed() + if is_entire_circuit: + c = c.with_irrelevant_tail_layers_removed() + return c.to_stim_circuit() diff --git a/glue/stimflow/src/stimflow/_layers/_transpile_test.py b/glue/stimflow/src/stimflow/_layers/_transpile_test.py new file mode 100644 index 00000000..4fcdf30b --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_transpile_test.py @@ -0,0 +1,635 @@ +import stim + +import stimflow + + +def test_to_cz_circuit_rotation_folding(): + assert stimflow.transpile_to_z_basis_interaction_circuit(stim.Circuit()) == stim.Circuit() + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + C_XYZ 0 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_XYZ 0 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + I 0 + TICK + C_XYZ 0 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_XYZ 0 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + C_XYZ 0 + TICK + I 0 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_XYZ 0 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + H 0 + TICK + H 0 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + H 0 1 2 3 4 5 + TICK + I 0 + H_YZ 1 + H_XY 2 + C_XYZ 3 + C_ZYX 4 + H 5 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_XYNZ 1 + C_ZYNX 2 + H 0 + S 4 + SQRT_X_DAG 3 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + H_XY 0 1 2 3 4 5 + TICK + I 0 + H_YZ 1 + H_XY 2 + C_XYZ 3 + C_ZYX 4 + H 5 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_NXYZ 5 + C_ZNYX 1 + H_XY 0 + SQRT_X 4 + SQRT_Y_DAG 3 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + H_YZ 0 1 2 3 4 5 + TICK + I 0 + H_YZ 1 + H_XY 2 + C_XYZ 3 + C_ZYX 4 + H 5 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_NZYX 5 + C_XNYZ 2 + H_YZ 0 + SQRT_Y 4 + S_DAG 3 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + C_XYZ 0 1 2 3 4 5 + TICK + I 0 + H_YZ 1 + H_XY 2 + C_XYZ 3 + C_ZYX 4 + H 5 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_XYZ 0 + C_ZYX 3 + SQRT_X_DAG 2 + SQRT_Y_DAG 1 + S_DAG 5 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + C_ZYX 0 1 2 3 4 5 + TICK + I 0 + H_YZ 1 + H_XY 2 + C_XYZ 3 + C_ZYX 4 + H 5 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_XYZ 4 + C_ZYX 0 + S 1 + SQRT_X 5 + SQRT_Y 2 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + I 0 1 2 3 4 5 + TICK + I 0 + H_YZ 1 + H_XY 2 + C_XYZ 3 + C_ZYX 4 + H 5 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + C_XYZ 3 + C_ZYX 4 + H 5 + H_XY 2 + H_YZ 1 + TICK + M 0 1 2 3 + """ + ) + ) + + +def test_to_cz_circuit_loop_boundary_folding(): + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + H 2 + TICK + CX rec[-1] 2 + TICK + S 2 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + CZ rec[-1] 2 + C_ZYX 2 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + MRX 0 + DETECTOR rec[-1] + TICK + M 0 + TICK + H 0 + """ + ) + ) + == stim.Circuit( + """ + H 0 + TICK + M 0 + TICK + R 0 + DETECTOR rec[-1] + TICK + H 0 + TICK + M 0 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + REPEAT 100 { + C_XYZ 0 + TICK + CZ 0 1 + TICK + H 0 + TICK + } + M 0 + """ + ) + ) + == stim.Circuit( + """ + H 0 + TICK + REPEAT 100 { + SQRT_X_DAG 0 + TICK + CZ 0 1 + TICK + } + H 0 + TICK + M 0 + """ + ) + ) + + +def test_to_cz_circuit_from_cnot(): + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + CX 0 1 2 3 + TICK + CX 1 0 2 3 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + H 1 3 + TICK + CZ 0 1 2 3 + TICK + H 0 1 + TICK + CZ 0 1 2 3 + TICK + H 0 3 + TICK + M 0 1 2 3 + """ + ) + ) + + +def test_to_cz_circuit_from_swap_cnot(): + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + CNOT 0 1 + TICK + SWAP 0 1 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + H 1 + TICK + CZSWAP 0 1 + TICK + H 0 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + CNOT 0 1 + TICK + SWAP 1 0 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + H 1 + TICK + CZSWAP 0 1 + TICK + H 0 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + SWAP 1 0 + TICK + CNOT 1 0 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + H 1 + TICK + CZSWAP 0 1 + TICK + H 0 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + SWAP 0 1 + TICK + CNOT 1 0 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + H 1 + TICK + CZSWAP 0 1 + TICK + H 0 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + CNOT 1 0 + TICK + SWAP 0 1 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + H 0 + TICK + CZSWAP 0 1 + TICK + H 1 + TICK + M 0 1 2 3 + """ + ) + ) + + assert ( + stimflow.transpile_to_z_basis_interaction_circuit( + stim.Circuit( + """ + SWAP 0 1 + TICK + CNOT 0 1 + TICK + M 0 1 2 3 + """ + ) + ) + == stim.Circuit( + """ + H 0 + TICK + CZSWAP 0 1 + TICK + H 1 + TICK + M 0 1 2 3 + """ + ) + ) + + +def test_tagged_layers_not_affected(): + builder = stimflow.ChunkBuilder(allowed_qubits=[0 + 0j]) + builder.append("tick") + builder.append("H", [0 + 0j]) + builder.append("tick") + builder.append("H", [0 + 0j], tag="tag") + actual = stimflow.transpile_to_z_basis_interaction_circuit(builder.circuit) + assert builder.q2i == {0: 0} + expected = stim.Circuit( + """ + H 0 + TICK + H[tag] 0 + """ + ) + assert actual == expected + + +def test_tagged_layers_not_affected_2q(): + + builder = stimflow.ChunkBuilder(allowed_qubits=[0 + 0j, 1 + 0j]) + builder.append("R", [0 + 0j, 1 + 0j]) + builder.append("tick") + builder.append("H", [0 + 0j]) + builder.append("tick") + builder.append("CNOT", [(0 + 0j, 1 + 0j)], tag="test") + builder.append("tick") + builder.append("M", [0 + 0j, 1 + 0j]) + actual = stimflow.transpile_to_z_basis_interaction_circuit(builder.circuit) + assert builder.q2i == {0: 0, 1: 1} + expected = stim.Circuit( + """ + R 0 1 + TICK + H 0 + TICK + CX[test] 0 1 + TICK + M 0 1 + """ + ) + assert actual == expected + + +def test_tagged_layers_simultaneous_ops(): + + builder = stimflow.ChunkBuilder(allowed_qubits=[0 + 0j, 1 + 0j, 2]) + builder.append("R", [0 + 0j, 1 + 0j]) + builder.append("tick") + builder.append("H", [0 + 0j]) + builder.append("tick") + builder.append("CNOT", [(0 + 0j, 1 + 0j)], tag="test") + builder.append("X", [2]) + builder.append("tick") + builder.append("M", [0 + 0j, 1 + 0j]) + actual = stimflow.transpile_to_z_basis_interaction_circuit(builder.circuit) + assert builder.q2i == {0: 0, 1: 1, 2: 2} + expected = stim.Circuit( + """ + R 0 1 + TICK + H 0 + X 2 + TICK + CX[test] 0 1 + TICK + M 0 1 + """ + ) + assert actual == expected + + +def test_tagged_layers_with_measurements(): + builder = stimflow.ChunkBuilder(allowed_qubits=[0 + 0j, 1 + 0j]) + builder.append("R", [0 + 0j, 1 + 0j]) + builder.append("tick") + builder.append("H", [0 + 0j]) + builder.append("tick") + builder.append("CNOT", [(0 + 0j, 1 + 0j)], tag="test") + builder.append("tick") + builder.append("M", [0 + 0j, 1 + 0j], tag="test again") + actual = stimflow.transpile_to_z_basis_interaction_circuit(builder.circuit) + assert builder.q2i == {0: 0, 1: 1} + expected = stim.Circuit( + """ + R 0 1 + TICK + H 0 + TICK + CX[test] 0 1 + TICK + M[test again] 0 1 + """ + ) + assert actual == expected diff --git a/glue/stimflow/src/stimflow/_viz/_3d_model.py b/glue/stimflow/src/stimflow/_viz/_3d_model.py new file mode 100644 index 00000000..8863792a --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_3d_model.py @@ -0,0 +1,453 @@ +from __future__ import annotations + +import base64 +import collections +from collections.abc import Iterable, Sequence +from typing import Any, cast + +import numpy as np + +from stimflow._viz._3d_model_text_texture import make_text_texture_data_uri +from stimflow._viz._3d_model_viewer import Viewable3dModelGLTF + + +class TextData: + def __init__( + self, + *, + text: str, + start: Sequence[float], + forward: Sequence[float], + up: Sequence[float], + mirror_backside: bool = True, + ): + """Describes a rectangle showing text. + + Args: + text: The text to draw in the rectangle. + start: The 3d point where the rectangle and text starts. + This is the `bottom_left` of the rectangle, in 3d. + forward: The 3d direction along which the text grows as the message gets longer. + This is the `bottom_right - bottom_left` of the rectangle, in 3d. + The length of this vector is ignored. + up: A 3d direction along which the text is oriented. + This is the `top_left - bottom_left` of the rectangle, in 3d. + The length of this vector is ignored. + Should be perpendicular to `forward`. + mirror_backside: Determines whether or not the text on the back of the rectangle + is mirrored (making it readable) or not (keeping the forward direction consistent). + Defaults to True (readable on both sides). + """ + self.text = text + self.start = np.array(start, dtype=np.float32) + self.forward = np.array(forward, dtype=np.float32) + self.up = np.array(up, dtype=np.float32) + self.mirror_backside = mirror_backside + + +class TriangleData: + def __init__(self, *, rgba: tuple[float, float, float, float], triangle_list: np.ndarray): + """Triangles with associated color information. + + Args: + rgba: Red, green, blue, and alpha data to associate with all the triangles. + Each value should range from 0 to 1. + (The alpha data is ignored in most viewers, but needed by the 3d model format.) + triangle_list: A 3d float32 numpy array with shape == (*, 3, 3). + Axis 0 is the triangle axis (each entry is a triangle). + Axis 1 is the ABC vertex axis (each entry is a vertex from the triangle). + Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). + """ + assert ( + len(triangle_list.shape) == 3 + and triangle_list.shape[1] == 3 + and triangle_list.shape[2] == 3 + and triangle_list.dtype == np.float32 + ) + assert len(rgba) == 4 + assert triangle_list.shape[0] > 0 + self.rgba: tuple[float, float, float, float] = cast(Any, tuple(rgba)) + self.triangle_list: np.ndarray = triangle_list + + @staticmethod + def rect( + *, + rgba: tuple[float, float, float, float], + origin: Iterable[float], + d1: Iterable[float], + d2: Iterable[float], + ) -> TriangleData: + """Creates a pair of triangles forming a rectangle. + + Args: + rgba: Color of the rectangle. + origin: Bottom-left corner of the rectangle. + d1: The right - left displacement. + d2: The top - bottom displacement. + """ + origin = np.array(origin, dtype=np.float32) + d1 = np.array(d1, dtype=np.float32) + d2 = np.array(d2, dtype=np.float32) + p1 = origin + d1 + p2 = origin + d2 + return TriangleData( + rgba=rgba, + triangle_list=np.array([ + [origin, p1, p2], + [p2, p1, p1 + d2], + ], dtype=np.float32) + ) + + @staticmethod + def fused(data: Iterable[TriangleData]) -> list[TriangleData]: + """Attempts to combine triangle data instances into fewer instances.""" + groups = collections.defaultdict(list) + for e in data: + groups[e.rgba].append(e) + result = [] + for rgba, group in groups.items(): + if len(group) == 1: + result.append(group[0]) + else: + result.append( + TriangleData( + rgba=rgba, + triangle_list=np.concatenate([e.triangle_list for e in group], axis=0), + ) + ) + return result + + +class LineData: + def __init__(self, *, rgba: tuple[float, float, float, float], edge_list: np.ndarray): + """Lines with associated color information. + + Args: + rgba: Red, green, blue, and alpha data to associate with all the lines. + Each value should range from 0 to 1. + (The alpha data is ignored in most viewers, but needed by the 3d model format.) + edge_list: A 3d float32 numpy array with shape == (*, 2, 3). + Axis 0 is the triangle axis (each entry is a triangle). + Axis 1 is the AB vertex axis (each entry is a vertex from the edge). + Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). + """ + assert ( + len(edge_list.shape) == 3 + and edge_list.shape[1] == 2 + and edge_list.shape[2] == 3 + and edge_list.dtype == np.float32 + ) + assert len(rgba) == 4 + assert edge_list.shape[0] > 0 + self.rgba: tuple[float, float, float, float] = cast(Any, tuple(rgba)) + self.edge_list: np.ndarray = edge_list + + @staticmethod + def fused(data: Iterable[LineData]) -> list[LineData]: + """Attempts to combine line data instances into fewer instances.""" + groups = collections.defaultdict(list) + for e in data: + groups[e.rgba].append(e) + result = [] + for rgba, group in groups.items(): + if len(group) == 1: + result.append(group[0]) + else: + result.append( + LineData( + rgba=rgba, edge_list=np.concatenate([e.edge_list for e in group], axis=0) + ) + ) + return result + + +def make_3d_model( + elements: Iterable[TriangleData | LineData | TextData], +) -> Viewable3dModelGLTF: + """Creates a 3d model containing the elements. + + Args: + elements: A list of objects to include in the model. The list can include triangles + (TriangleData), lines (LineData), and text (TextData). + + Returns: + The 3d model, as a `stimflow.gltf_model`. + + `stimflow.gltf_model` inherits from `pygltflib.GLTF2` but adds a `_repr_html_` class + (creating a 3d viewer in Jupyter notebooks) and a `write_viewer_to` method for + saving a standalone HTML viewer. + """ + import pygltflib + + triangles: list[TriangleData] = [] + lines: list[LineData] = [] + texts: list[TextData] = [] + for item in elements: + if isinstance(item, TriangleData): + triangles.append(item) + elif isinstance(item, LineData): + lines.append(item) + elif isinstance(item, TextData): + texts.append(item) + else: + raise NotImplementedError(f'make_gltf_model {item=}') + + triangles = TriangleData.fused(triangles) + lines = LineData.fused(lines) + + gltf = Viewable3dModelGLTF() + gltf.asset = {"version": "2.0"} + + def add_obj_index(x: list[Any], v: Any): + x.append(v) + return len(x) - 1 + + list_coords_text_triangles: list[np.ndarray] = [] + list_uv_coords_text_triangles: list[list[float]] = [] + for t in texts: + a = t.start + b = a + t.forward * 0.5 * len(t.text) + c = a + t.up + d = b + t.up + lines.append( + LineData( + rgba=(0, 0, 0, 1), edge_list=np.array([[a, b], [a, c], [b, d], [c, d]], dtype=np.float32) + ) + ) + for direction in [-1, +1]: + a = t.start.copy() + if direction == -1: + a += t.forward * 0.5 * len(t.text) + seq = t.text + if direction == -1 and not t.mirror_backside: + seq = seq[::-1] + for char in seq: + b = a + t.forward * 0.5 * direction + c = a + t.up + d = b + t.up + o = ord(char) + if o >= 128: + o = 0 + u0: float = o % 16.0 + v0: float = o // 16 + u1: float = u0 + 1 + v1: float = v0 + 1 + u0 /= 16 + u1 /= 16 + v0 /= 8 + v1 /= 8 + ua = [u0, v1] + ub = [u1, v1] + uc = [u0, v0] + ud = [u1, v0] + list_coords_text_triangles.extend([a, b, c, c, b, d]) + if direction == -1 and not t.mirror_backside: + list_uv_coords_text_triangles.extend([ub, ua, ud, ud, ua, uc]) + else: + list_uv_coords_text_triangles.extend([ua, ub, uc, uc, ub, ud]) + a = b + + lines = LineData.fused(lines) + + coords_tri = ( + np.array([]) + if not triangles + else np.concatenate([data.triangle_list for data in triangles], axis=0) + ) + coords_edg = ( + np.array([]) + if not lines + else np.concatenate([data.edge_list for data in lines], axis=0) + ) + coords_text_triangles = np.array(list_coords_text_triangles, dtype=np.float32) + uv_coords_text_triangles = np.array(list_uv_coords_text_triangles, dtype=np.float32) + + coord_data = ( + coords_tri.tobytes() + + coords_edg.tobytes() + + coords_text_triangles.tobytes() + + uv_coords_text_triangles.tobytes() + ) + buffer_bytes_b64 = base64.b64encode(coord_data).decode() + shared_buffer_index = add_obj_index( + gltf.buffers, + pygltflib.Buffer( + uri=f"data:application/octet-stream;base64,{buffer_bytes_b64}", + byteLength=len(coord_data), + ), + ) + + byte_offset = 0 + mesh0 = pygltflib.Mesh() + for tri_data in triangles: + material_index = add_obj_index( + gltf.materials, + pygltflib.Material( + pbrMetallicRoughness=pygltflib.PbrMetallicRoughness( + baseColorFactor=list(tri_data.rgba), roughnessFactor=0.8, metallicFactor=0.3 + ), + emissiveFactor=None, + doubleSided=True, + ), + ) + byte_length = tri_data.triangle_list.shape[0] * 3 * 3 * 4 + buffer_view_index = add_obj_index( + gltf.bufferViews, + pygltflib.BufferView( + buffer=shared_buffer_index, + byteOffset=byte_offset, + byteLength=byte_length, + target=pygltflib.ARRAY_BUFFER, + ), + ) + byte_offset += byte_length + accessor_index = add_obj_index( + gltf.accessors, + pygltflib.Accessor( + bufferView=buffer_view_index, + byteOffset=0, + componentType=pygltflib.FLOAT, + count=tri_data.triangle_list.shape[0] * 3, + type=pygltflib.VEC3, + max=[float(e) for e in np.max(tri_data.triangle_list, axis=(0, 1))], + min=[float(e) for e in np.min(tri_data.triangle_list, axis=(0, 1))], + ), + ) + mesh0.primitives.append( + pygltflib.Primitive( + material=material_index, + mode=pygltflib.TRIANGLES, + attributes=pygltflib.Attributes(POSITION=accessor_index, TEXCOORD_0=accessor_index), + ) + ) + for line_data in lines: + material_index = add_obj_index( + gltf.materials, + pygltflib.Material( + pbrMetallicRoughness=pygltflib.PbrMetallicRoughness( + baseColorFactor=list(line_data.rgba), roughnessFactor=0.8, metallicFactor=0.3 + ) + ), + ) + + byte_length = line_data.edge_list.shape[0] * 3 * 2 * 4 + buffer_view_index = add_obj_index( + gltf.bufferViews, + pygltflib.BufferView( + buffer=shared_buffer_index, + byteOffset=byte_offset, + byteLength=byte_length, + target=pygltflib.ARRAY_BUFFER, + ), + ) + byte_offset += byte_length + accessor_index = add_obj_index( + gltf.accessors, + pygltflib.Accessor( + bufferView=buffer_view_index, + byteOffset=0, + componentType=pygltflib.FLOAT, + count=line_data.edge_list.shape[0] * 2, + type=pygltflib.VEC3, + max=[float(e) for e in np.max(line_data.edge_list, axis=(0, 1))], + min=[float(e) for e in np.min(line_data.edge_list, axis=(0, 1))], + ), + ) + mesh0.primitives.append( + pygltflib.Primitive( + material=material_index, + mode=pygltflib.LINES, + attributes=pygltflib.Attributes(POSITION=accessor_index), + ) + ) + + if texts: + text_image_index = add_obj_index( + gltf.images, pygltflib.Image(uri=make_text_texture_data_uri()) + ) + text_sampler_index = add_obj_index( + gltf.samplers, + pygltflib.Sampler( + magFilter=pygltflib.NEAREST_MIPMAP_NEAREST, + minFilter=pygltflib.NEAREST_MIPMAP_NEAREST, + wrapS=pygltflib.CLAMP_TO_EDGE, + wrapT=pygltflib.CLAMP_TO_EDGE, + ), + ) + text_texture_index = add_obj_index( + gltf.textures, pygltflib.Texture(sampler=text_sampler_index, source=text_image_index) + ) + text_material_index = add_obj_index( + gltf.materials, + pygltflib.Material( + pbrMetallicRoughness=pygltflib.PbrMetallicRoughness( + metallicFactor=0.1, + roughnessFactor=0.9, + baseColorTexture=pygltflib.TextureInfo(index=text_texture_index), + ), + doubleSided=False, + ), + ) + byte_length = coords_text_triangles.shape[0] * 3 * 4 + buffer_view_index = add_obj_index( + gltf.bufferViews, + pygltflib.BufferView( + buffer=shared_buffer_index, + byteOffset=byte_offset, + byteLength=byte_length, + target=pygltflib.ARRAY_BUFFER, + ), + ) + byte_offset += byte_length + accessor_index = add_obj_index( + gltf.accessors, + pygltflib.Accessor( + bufferView=buffer_view_index, + byteOffset=0, + componentType=pygltflib.FLOAT, + count=coords_text_triangles.shape[0], + type=pygltflib.VEC3, + max=[float(e) for e in np.max(coords_text_triangles, axis=0)], + min=[float(e) for e in np.min(coords_text_triangles, axis=0)], + ), + ) + + byte_length = uv_coords_text_triangles.shape[0] * 2 * 4 + buffer_view_index_2 = add_obj_index( + gltf.bufferViews, + pygltflib.BufferView( + buffer=shared_buffer_index, + byteOffset=byte_offset, + byteLength=byte_length, + target=pygltflib.ARRAY_BUFFER, + ), + ) + byte_offset += byte_length + accessor_index_2 = add_obj_index( + gltf.accessors, + pygltflib.Accessor( + bufferView=buffer_view_index_2, + byteOffset=0, + componentType=pygltflib.FLOAT, + count=uv_coords_text_triangles.shape[0], + type=pygltflib.VEC2, + max=[float(e) for e in np.max(uv_coords_text_triangles, axis=0)], + min=[float(e) for e in np.min(uv_coords_text_triangles, axis=0)], + ), + ) + mesh0.primitives.append( + pygltflib.Primitive( + material=text_material_index, + mode=pygltflib.TRIANGLES, + attributes=pygltflib.Attributes( + POSITION=accessor_index, TEXCOORD_0=accessor_index_2 + ), + ) + ) + + mesh0_index = add_obj_index(gltf.meshes, mesh0) + node0 = pygltflib.Node(mesh=mesh0_index) + node0_index = add_obj_index(gltf.nodes, node0) + gltf.scenes.append(pygltflib.Scene(nodes=[node0_index])) + + return gltf diff --git a/glue/stimflow/src/stimflow/_viz/_3d_model_test.py b/glue/stimflow/src/stimflow/_viz/_3d_model_test.py new file mode 100644 index 00000000..fe26ad4f --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_3d_model_test.py @@ -0,0 +1,121 @@ +import json + +import numpy as np + +import stimflow +from stimflow._viz._3d_model import TextData + + +def test_make_3d_model(): + model = stimflow.make_3d_model( + [ + stimflow.TriangleData( + rgba=(1, 0, 0, 1), + triangle_list=np.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]], dtype=np.float32), + ), + stimflow.TriangleData( + rgba=(1, 0, 1, 1), + triangle_list=np.array([[[1, 1, 0], [0, 1, 0], [0, 0, 1]]], dtype=np.float32), + ), + ] + ) + assert json.loads(model.to_json()) == { + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 3, + "max": [1.0, 1.0, 1.0], + "min": [0.0, 0.0, 0.0], + "normalized": False, + "type": "VEC3", + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 3, + "max": [1.0, 1.0, 1.0], + "min": [0.0, 0.0, 0.0], + "normalized": False, + "type": "VEC3", + }, + ], + "asset": {"version": "2.0"}, + "bufferViews": [ + {"buffer": 0, "byteLength": 36, "byteOffset": 0, "target": 34962}, + {"buffer": 0, "byteLength": 36, "byteOffset": 36, "target": 34962}, + ], + "buffers": [ + { + "byteLength": 72, + "uri": "data:application/octet-stream;base64,AACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAA" + "AAAAAAAAAIA/AACAPwAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/", + } + ], + "materials": [ + { + "alphaMode": "OPAQUE", + "doubleSided": True, + "pbrMetallicRoughness": { + "baseColorFactor": [1, 0, 0, 1], + "metallicFactor": 0.3, + "roughnessFactor": 0.8, + }, + }, + { + "alphaMode": "OPAQUE", + "doubleSided": True, + "pbrMetallicRoughness": { + "baseColorFactor": [1, 0, 1, 1], + "metallicFactor": 0.3, + "roughnessFactor": 0.8, + }, + }, + ], + "meshes": [ + { + "primitives": [ + {"attributes": {"POSITION": 0, "TEXCOORD_0": 0}, "material": 0, "mode": 4}, + {"attributes": {"POSITION": 1, "TEXCOORD_0": 1}, "material": 1, "mode": 4}, + ] + } + ], + "nodes": [{"mesh": 0}], + "scenes": [{"nodes": [0]}], + } + + +def test_make_3d_model_html_viewer(): + model = stimflow.make_3d_model( + [ + stimflow.TriangleData( + rgba=(1, 0, 0, 1), + triangle_list=np.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]], dtype=np.float32), + ), + stimflow.TriangleData( + rgba=(1, 0, 1, 1), + triangle_list=np.array([[[1, 1, 0], [0, 1, 0], [0, 0, 1]]], dtype=np.float32), + ), + ] + ) + + html = model.html_viewer() + assert "" in html + + +def test_3d_text(): + model = stimflow.make_3d_model( + [ + stimflow.TriangleData( + rgba=(1, 0, 0, 1), + triangle_list=np.array([[[0, 0, 0], [1, 0, 0], [0, 1, 0]]], dtype=np.float32), + ), + stimflow.LineData( + rgba=(0, 0, 1, 1), edge_list=np.array([[[0, 0, 0], [1, 1, 1]]], dtype=np.float32) + ), + TextData(text="test", start=[0, 0, 0], forward=(1, 0, 0), up=(0, 1, 0)), + ] + ) + assert model is not None diff --git a/glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.html b/glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.html new file mode 100644 index 00000000..2cd7ab0b --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.html @@ -0,0 +1,57 @@ + + + + + + \ No newline at end of file diff --git a/glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.py b/glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.py new file mode 100644 index 00000000..b8092b10 --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_3d_model_text_texture.py @@ -0,0 +1,2185 @@ +def make_text_texture_data_uri() -> str: + """An image with letters and digits and etc that can be textured onto triangles in the model. + + To produce this data: + 1. Open stimflow/_viz/_3d_model_text_texture.html in a web browser. + 2. Right-click the canvas and copy its image to your clipboard. + 3. Copy the contents into an image editor like GIMP. + 4. Save/export the image as a PNG file. + 5. Run `base64 < your_saved_image.png`. + 6. Copy the output of that command into the `TEXTURE_PNG_DATA` string below. + """ + return "data:image/png;base64," + TEXTURE_PNG_DATA.replace("\n", "") + + +TEXTURE_PNG_DATA = """ +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 +/Pjx48ePHz9+/PgDyS9f7ITTklHLBXwFyThsgycfP378+PHjx48fP378+PHjDxS/fLUTLknGYxbw +90vG3zZ68vHjx48fP378+PHjx48fP/5A8MuXO+GqZEQ7wT92fUcZNr3hx48fP378+PHjx48fP378 +/vLL1zshVTJ63ALf5voOMmx+w48fP378+PHjx48fP378+P3hl792xFs34Htc3zFGJrrhx48fP378 ++PHjx48fP378vrw5jGsPwi99KOmUpFhlzvDjx48fP378+PHjx48fP35f5dcBABERERERERH5phB2 +AREREREREREDACIiIiIiIiJiAEBEREREREREDACIiIiIiIiIiAEAERERERERETEAICIiIiIiIiIG +AERERERERETEAICIiIiIiIiIGAAQERERERERMQAgIiIiIiIiIgYARERERERERMQAgIiIiIiIiIgY +ABARERERERERAwAiIiIiIiIiYgBARERERERERAwAiIiIiIiIiIgBABEREREREREDACIiIiIiIiJi +AEBEREREREREDACIiIiIiIiIiAEAERERERERETEAICIiIiIiIiIGAEREREREREQUtAOA7ZJ+ysQ7 +Hz9+/Pjx48ePHz9+/Pjx47f9AOBnSfUkNZE0OxM++fjx48ePHz9+/Pjx48ePH79PMyTD17cFkpFd +MnT9FiIZ4/3wOPx1w48fP378+PHjx48fP378+H198/kAYKpkhN2Av/H2n0zw5OPHjx8/fvz48ePH +jx8/fvz/sfsAYKRkONLBp906SkaKTZ98/Pjx48ePHz9+/Pjx48eP319+nw0A+pvAb7w9KxmJNnvy +8ePHjx8/fvz48ePHjx8/fn/6vT4ASJaMGBfwabeHJeO8DZ54/Pjx48ePHz9+/Pjx48ePPxD8Xh0A +XJGMpm7g0273SsZfQfzk48ePHz9+/Pjx48ePHz9+/IHi99oA4Kxk1M0APu12l2QcDMInHz9+/Pjx +48ePHz9+/Pjx4w8kv1cGAMcko7IH8Gm3wpKxPYiefPz48ePHjx8/fvz48ePHjz/Q/B4fAOyVjJIe +xKfd8krGmiB48vHjx48fP378+PHjx48fP/5A9Ht0ALBJMqK8gE+75ZCM7wL4ycePHz9+/Pjx48eP +Hz9+/PgD1e+xAcByycjlRXzaLUwyPg/AJx8/fvz48ePHjx8/fvz48eMPZL9HBgDfSka4D/BpN4dk +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 +P35/+H0+AHjR4rq1kmJlv/Djx48fP378+PHjx48fP35/+H06AMgjqaHFtV107eQHdgo/fvz48ePH +jx8/fvz48eP3l9+nA4C6kkItrPtN0ibZL/z48ePHjx8/fvz48ePHj99ffp8OAO63uG6+7Bl+/Pjx +48ePHz9+/Pjx48fvL79PBwClLa5bZdMXAH78+PHjx48fP378+PHjx+8vv08HACUsrou36QsAP378 ++PHjx48fP378+PHj95ffpwOA3BbXnbTpCwA/fvz48ePHjx8/fvz48eP3l9+nA4AcFtedt+kLAD9+ +/Pjx48ePHz9+/Pjx4/eX36cDgCwW1xk2fQHgx48fP378+PHjx48fP378/vKHiIiIiIiIiIhsHwMA +IiIiIiIiIgYARERERERERMQAgIiIiIiIiIgYABARERERERERAwAiIiIiIiIiYgBARERERERERJ4q +zJd3VkqSw8K6FJvubPz48ePHjx8/fvz48ePHj99ffp8OAC4rc4cfP378+PHjx48fP378+PH7K94C +QERERERERJQJYgBARERERERElBkHAFVsjK3ioTX48ePHjx8/fvz48ePHjx9/0PkNybjxliIZnSVD +Nrs1kYzLN1lvdcOPHz9+/Pjx48ePHz9+/Pjt6Fd6O2KIjfBtJSPJwpOPHz9+/Pjx48ePHz9+/Pjx +29XvMK4tumWTJHWTlOqBww8KSLrP4trfJB330GEPfSSNlLXLL+DHjx8/fvz48ePHjx8/fvy29ZtN +QmZJRlYPTCEedmH60txDk48RLk598OPHjx8/fvz48ePHjx8/frv6Ta8C0ErSQkkRCp5CJX0iaYAH +toUfP378+PHjx48fP378+PHbwW/pMoANJS2TlD8I8NklfSMpxoPbxI8fP378+PHjx48fP378+IPd +H2J1wzUl/SypWADj80haLKmpF7aNHz9+/Pjx48ePHz9+/PjxB7M/xJU7KCfpF0nlAxB/u6RVkup4 +8T7w48ePHz9+/Pjx48ePHz/+YPWHuHpHRSWtlvUzGvqi0pLWSKrkg/vCjx8/fvz48ePHjx8/fvz4 +g9Ef4s4d5pe0VFKjAMDfcx1f0of3iR8/fvz48ePHjx8/fvz48QebP8TdO46QtEBSaz/iG0haIek2 +P9w3fvz48ePHjx8/fvz48ePHH0z+kIw8gCySpkvq5gd8c0k/SMrpxycAP378+PHjx48fP378+PHj +DxZ/SEYfiEPSOElDfYh/RdIcSeHyf/jx48ePHz9+/Pjx48ePH38w+EM89aD+I2myJzeYToMlve+D ++8GPHz9+/Pjx48ePHz9+/Pjt5Pfo4+0oaZakrF6Ap01a3lTghh8/fvz48ePHjx8/fvz48QeqP8zT +D7SFpLySnpZ08Yb/nizprMVtJN30/7NI+kz+PeECfvz48ePHjx8/fvz48ePHH9R+QzK8cdsgGQUl +Qxm8RUjGj156jN684cePHz9+/Pjx48ePHz9+/IHklzd3wu+SUTwD+PySsS4In3z8+PHjx48fP378 ++PHjx48/0Pzy9k44Ihl3u4EvJhm7gvjJx48fP378+PHjx48fP378+APJL1/shNOSUcsFfAXJOGyD +Jx8/fvz48ePHjx8/fvz48eMPFL98tRMuScZjFvD3S8bfNnry8ePHjx8/fvz48ePHjx8//kDwy5c7 +4apkRDvBP3Z9Rxk2veHHjx8/fvz48ePHjx8/fvz+8svXOyFVMnrcAt/m+g4ybH7Djx8/fvz48ePH +jx8/fvz4/eGXv3bEWzfge1zfMUYmuuHHjx8/fvz48ePHjx8/fvy+vDmMaw/CL30o6ZSkWGXO8OPH +jx8/fvz48ePHjx8/fl/l1wEAEREREREREfmmEHYBEREREREREQMAIiIiIiIiImIAQEREREREREQM +AIiIiIiIiIiIAQARERERERERMQAgIiIiIiIiIgYARERERERERMQAgIiIiIiIiIgYABAREREREREx +ACAiIiIiIiIiBgBERERERERExACAiIiIiIiIiBgAEBEREREREREDACIiIiIiIiJiAEBERERERERE +DACIiIiIiIiIiAEAEREREREREQMAIiIiIiIiIrJpYTIM9gIRERERERGRzeMIACIiIiIiIiIGAERE +RERERETEAICIiIiIiIiIGAAQEREREREREQMAIiIiIiIiImIAQEREREREREQMAIiIiIiIiIiIAQAR +ERERERERMQAgIiIiIiIiYgBARERERERERAwAiIiIiIiIiIgBABERERERERExACAiIiIiIiIiBgBE +RERERERExACAiIiIiIiIiBgAEBEREREREREDACIiIiIiIiIGAERERERERETEAICIyEPFxcXJ4XD8 +65aQkMDOIcrE9enT55bfG9JuVatWVWpqakA95pdfftnpY27QoAFPLBERBUwOwzAMdgMR+XoAUKlS +pX/998uXLyt79uzsIKJM2M6dO1WlShUlJ47ICu8AACAASURBVCenu2bRokV69NFHA+pxHzt2TKVL +l9aVK1fSXTNjxgy1bt060z/HdevWVXx8/L/++9q1a1WgQAG+CIiIfFAYu4AyWkpKipKSkpyuCQ0N +VZYsWTy2vbCwMIWF8fK1W8wj7dWxY8e0fPlybd68WTt27NCRI0d0/PhxXb58WQkJCcqWLZsiIiIU +GRmpqKgolSpVSqVKlVLZsmVVs2ZNlS5d2jb7Ijk52ekvtnxfk7p27ep0Hz3yyCMB98u/JBUuXFg9 +evTQiBEj0l3Tt29fPfnkk4qMjMzU3xMOHDigo0eP3vLrg4iIfBNvAfBRBw8edHqI4I23MWPGBJXt +008/Vfbs2Z3eXPnLx+jRo02317FjR15UDAAoADt9+rRGjx6tatWqqUiRInr++ec1atQo/fjjj4qL +i9OpU6d0+fJlpaam6vLlyzp58qQOHDigtWvXavr06Ro6dKiio6NVpkwZFSxYUE2bNtXYsWO1b9++ +oN4vw4YNM/2+1rdv30z7upk1a5aWL1+e7scdDofefvvtgH38AwcOVP78+dP9+LFjx/TGG2/wDYKI +iNwqMTFRt99+u+nvkbfddpvTI9IYABD5sLJlyzr9gh06dKjL22zatKnTbbZt2zao9lGgvbeXrHfx +4kUNGjRIJUqUUO/evbVly5YMb/PUqVNasGCBevbsqTJlyqhcuXL69ttv2dk2fO2YDT/atGmje+65 +J2ANuXPn1qBBg5yuGTt2rHbt2sUTTkRELjd16lQdP37cdF337t1N307LAIDIBx09elR79uxxuuah +hx5yaZspKSlatWqV0zUPP/xwUO0njgAIzpYtW6a7775bw4cP14ULF7x2P7t379Zvv/3GDrdZQ4cO +veVh4WllzZpVw4YNC3hHly5dVLx48XQ/npSUpG7duvGEExGRS6Wmpurdd981XRcZGanOnTubrmMA +QOSjX5CcFRERofvuu8+lbW7cuFHnzp3z6FCBAYDzzp8/r1mzZqlTp06qXbu2ChcurBw5cigsLEy5 +c+dWuXLl9NRTT+mdd97R9u3bM8Vre8yYMXr00Uf1559/8oVOLnfgwAHTt73FxMSoRIkSAW8JDw9X +bGys0zVLly7VvHnzeOKJiMhyX3/9taW3Qr788svKly8fAwCiYBgA1KlTx/JJEm/8QdJZZcqUUbFi +xRgAeKAdO3aobdu2ioqKUuvWrTVlyhT98ssvio+P15UrV5SSkqLz589r9+7dmj9/vgYMGKDKlSur +SpUqmjp1qulJLYO1t956S7169VJKSgpf5ORWQ4YMcfr1ERoaqn79+gWNp3379ipUqJDTNYMHD+bt +TkREZDkr58DJkiWLevXqZWl7DACIfJCzk1tJ7v2l3myoEGyH/0uBdw6ACxcuqEuXLqpcubKmTZum +hIQElz5/27ZtiomJUcWKFbV48WJbvaanT5+uwYMH88VNbrdz505Nnz7d6ZoWLVqoVKlSQWMKDw9X +z549na6Ji4vTzJkzeQEQEZFpP/30k6W3P7Zp08byH/4YABB5uf379+vQoUMeHQAkJiZqzZo1thsA +BNIRANu2bdM999yjSZMmZXgwsWfPHjVq1Ei9e/e2xeWuDh48qFdeeYUvbspQVv4SPnDgwKBzderU +Sblz53a6ZsiQIVz6joiITLPy13+Hw+HS0XIMAIi8nNlf6vPkyePy2a3XrFnj9K/RDodDDRo0YADg +ZqtWrVK9evU8fum50aNHq1mzZqaXZwn0unfvrsuXL7v0OTVq1NCgQYM0d+5cbd26VYcOHdLJkye1 +Z88erVu3Tj/88IPeffddtWjRIujeukKut3HjRtMrOjRu3FhVqlQJOluuXLlMT8K0f/9+ffzxx0Fj +mjJlitatWxewj+/QoUNcZpGIbNemTZv0008/ma574okndPfdd1vebhi7lsi/A4AHH3xQISEhHt1m +lSpVnF6TmgGA819MmjRpoosXL3pl+999951atmypb7/9VmFhwfct+LffftOCBQssr2/SpImGDRum +qlWr3vLjBQoU+Od/P/bYY//873379mnWrFn6+OOPdfDgQb6R2CyzS+ZJMj2hXiDXs2dPjR492umg +9s0331S7du2ULVu2gLbs379fPXv21NWrV9WhQweNGDHC0kmmfNHVq1f17rvvatiwYbpy5YqqVq2q +p556ii8wIrJFI0aMsLRuwIABLm2XIwCIvNyKFSucftyd9/+bnQAwUA//P3PmjLZv365ff/31lh8/ +ePCg/vrrL7/9hfyvv/5S06ZNvfbLf1oLFy5U//79g/L1PHnyZEvrwsLC9PHHH2vhwoXp/vLvrNKl +S2vw4MHat2+f5syZo/Lly/PNxCb9+uuvWrJkidM1VatWVd26dYPWeNttt6lVq1ZO1xw9elQfffRR +wFv69u2rxMREGYahDz/8UGXLltXHH3/s94HtkiVLVKlSJb322mv//JvRt29fXb16lS8yIgr69u/f +r6+//tp0Xe3atVW7dm0GAESB0o4dO3T8+HGPDgAuXLigjRs3enyo4OnOnTunefPmqV+/fqpfv75u +u+025cuXT5UrV073/eM1a9ZUVFTUP5fWi4qKUp06dRQTE6Phw4fr22+/VXx8vNcec4cOHSxtv3Tp +0urXr5++++47bdmyRfv379fatWv1xRdfKDo6WpGRkabbGD16dNCdGDAlJcXSP0aSNG3aNL344osZ +vs/Q0FC1aNFC27dv1/jx45UnTx6+sQR5o0aNMl1jh3NMvPzyy6ZrxowZE9BXBFi2bJnmzp37P//t +1KlT6tChgx544AFt2bLF54/p6NGjatWqlR599FHt2bPnfz62b98+jR07li8yIgr6Ro4caenfB1f/ ++i9JMsgnHThwwJBk6TZ69Oigsn344YempubNm1ve3siRI02399JLLwXFvhk/frxTR1RUlMvbXLBg +gdNthoWFGRcuXPCLNyEhwfjiiy+MRo0aGVmzZrX8mnf1VqJECaN169bGhAkTjL1793rksZvtV0lG +gQIFjE8++cRITk52uq0TJ04YnTt3Nt1e6dKljcTExKD5Wt+wYYOl56ddu3Z807+pIUOGmO63Hj16 +ZIp/C0NDQ53uhxw5chjnzp2zhbdChQqmz/s333wTkI89OTnZqFy5stPHHhoaanTr1s04e/aspW0W +KVLkltuJj483/dykpCRj1KhRRmRkpNPHlCtXLuPEiRN80yGioO348eNGtmzZTP/9qFChgpGamury +9jkCgMjLfz1xljsn6jM7/P++++6z9BdoT3bp0iWNGDFCRYsW1fPPP69FixZ59TDMgwcPaubMmera +tavKlCmju+66Sz179szQX9Rff/11px+vXLmyNmzYoJiYGIWGhjpde9ttt2nixImaM2eOcuTIke66 +ffv26fPPPw+a1/OGDRtM1zgcDg0bNowvfkr3L94pKSlO17Rq1Uq5cuWyhdfKUQDvvfdeQD72gwcP +6sKFC07XpKSkaPz48SpXrpzpJR0z0urVq1WtWjX16dPH9C1aoaGh2rp1K19sRBS0jR071tKlp/v3 +7y+Hw+Hy9hkAEHmp1NRUrVy50ukab7z/39eH/3/zzTcqU6aMYmNjderUKb/s671792rs2LFq1KiR +W5fWWrt2rdNrrJYoUUJLlixRiRIlXNpuixYtNGPGDKcneRw9enTQvKatXBXh/vvvV9GiRfkGQP/q +3Llz+uSTT0zX2ekSk23btlV4eLjTNT///LOl4ZqvK1WqlH7//XeNHj3a9KSyx48f1/PPP6/69etr +586dHnsMf/31l9q1a6d69eopLi7O6drw8HD16dNH+/fvV8OGDfmCI6Kg7MKFC5bOt1S0aFG1adPG +rftgAEDkpbZs2aLTp0979Jf1kydPmv4Q5KsTACYkJKh9+/Zq3ry5V9+X74tmz56d7sccDoe+/PJL +3XbbbW5t+6mnnnJ6LfOdO3dq06ZNQbGfTpw4YbrG1UtaUubpww8/NP2LcqVKlXT//ffbxpwvXz49 +88wzpusC9SiArFmzqmfPntq/f78GDhyo7NmzO12/cuVKVa1aVf3799elS5fcvt/U1FRNmjRJZcuW +NT1KyuFw6Pnnn9eePXv07rvvKm/evHyxEVHQNmXKFJ09e9Z0Xe/evZUlSxYGAESBlNnh/3fccYdK +lSrl8jadnXk5e/bsqlWrltdtZ86c0YMPPqjPPvvMFs+Vs6MqWrdurerVq2do+7GxsYqKikr34z/+ ++GNQ7CcrP9AXKlSIL376V4ZhaNKkSabr3P1rRiAXHR1tuuarr74yPWGsP8udO7eGDx+uvXv3KiYm +xulRTUlJSRo5cqTKlStn+aShN7Z+/XrVqFFDXbp0Mf0huGHDhtq0aZOmTZumO+64gy80Igrqrl69 +qjFjxpiuy5s3r6W3mKVXGLuayD8DAG8c/l+nTh1lzZrVq64LFy7ooYce8svZn71RQkKCduzYke7H +O3TokOH7iIyMVJs2bdI93H/NmjVBsa+svM/M35cGo8Bs1apVOnDggOm65s2b287esGFD5cqVS+fP +n093TXJysmbMmKHevXsHtKVIkSL65JNP1Lt3bw0cOFDfffddumuPHDmiZ599Vo0aNdKECRNUunRp +p9s+ffq0YmNj9dFHH5me+bpatWp655139MgjjwTFa+DYsWNatWqVqals2bJ8syDKxE2bNk3Hjh0z +XdelS5cMne+LAQCRF0pOTtbq1as9PgAwGyp4+/B/wzDUpk0bl375z58/v5o1a6a6devq3nvvVf78 ++RUfH6977733lsOFlJQUnTlzRocPH9Yff/yhzZs3a8OGDVq/fr1b7+8368CBA+n+sBkREeGxa5E/ +/vjj6Q4Abr6UVaAWERFhuubkyZN8A6B/ZeVkl5UqVVKZMmVsZ8+aNaueeOIJzZgxw3QfBfoAIK2K +FStq4cKFWrFihfr37+/0HAaLFi1SxYoV1b9//3S/h0+dOlWjRo0yPY9M8eLFNWzYMEVHR7t14it/ +tWnTJrVu3drpmpEjRzIAIMrEpaamauTIkabrsmfPru7du2fovhgAEHmh9evXm56p2NUBwKFDh7R/ +/36/DgAmTpyohQsXWlqbL18+vf7663rppZf+dSb8v//++9bfkMLCFBkZqdy5c6tEiRKqW7eu2rVr +989wYPny5frqq680f/58nTt3ziOm9B6LJBUrVkxhYZ75Nuns7R5W3lsfCN1+++2ma5wdTUGZsytX +ruirr74yXWfHv/7faDMbAGzdulXbtm1T5cqVg8ZVv359rVu3TnPmzNFrr72W7r9RiYmJevPNN9Pd +TmxsrOm/J4MGDVLXrl1NT6pIRBSMzZ07V7t37zZdFxMTo4IFC2bovjgHAJEXMvtL/V133aUiRYq4 +tE2zw//z5Mnj1ROwnTx5UoMGDbK0tkaNGtq0aZO6devm9DJ4rpQzZ041bdpUn3/+uY4fP67p06e7 +dRnFW/1y4uyHTk/l7Czaly9fDorX9Z133mm6Zv369V69BCQF5w81zg5/zwwDgMcee8zS98Jguixo +Wg6HQ61atdKuXbs0duxYFShQwGPbzpYtm/r166f9+/erT58+/PJPRLbt7bffNl0TGhqqPn36ZPi+ +GAAQ+WEA4I3D/+vXr+/0xEwZbdSoUaZn8JakypUra+nSpSpevLjXHku2bNnUpk0bLVu2TNu2bVP7 +9u3dPhNqtmzZ0v2Yp44ykK6dONGdxxBIWRkwXbx40fS9rpS5mjZtmumaMmXKqGLFirbdBzly5FDj +xo1N182YMUMpKSlBacySJYu6d++u/fv3a9CgQRka/oaEhKht27bavXu33nnnHeXJk4cvJCKybStW +rND69etN17Vo0cLSH2MYABD5uMTERP36668+HwC4s02rJSUl6aOPPjJdFxkZqQULFihnzpw+29+V +KlXSp59+qj179qhDhw4uvy/U2SWjjh496rGT2h06dCjdj3nySANvVrVqVdPLgEnSp59+yjcCknRt +8LVkyRLTdY0aNbL9vrBijI+PD/oBWq5cufTWW29p7969eumllxQaGuryftq8ebM+++wzzuxPRJmi +ESNGWFrXv39/j9wfAwCiW1SoUCE5HA63btmyZVNCQoLT7bds2dLl7cbHxzvdZvfu3V3eptUWLVrk +9L3yafXq1ctvP7CVKFFCH374ocs/bJYsWTLdj509e1YbN270yONbtGhRuh9z9XKQ/ipr1qyWzrr9 +9ddfm75eKXO0ePFiSyfvrF+/vu33hVXjDz/8YAtv4cKF9dFHH2nlypWWTiAqSR988IF+/PHHoDoP +AhFRRtq6davTnxHTatSokapVq8YAgIh8k9n5B6RrJ/Dr1atX0NkiIiKcnnnZ7MRdVrp69arT7dzq +igiB2rPPPmu6JjEx0dJ72cj+Wfll1uFw6MEHH7T9vihTpoyKFi2aaQYAiYmJeu+999S0aVNdunTJ +0ud07dpVvXr1sjRwJiKyQ1Z/XhowYIDH7pMBABGZtnbtWtM1tWrVcno4fSDn7GSCU6ZM0eHDhzO0 +/QkTJujgwYPpftybb9/wxgAgV65cpuvef/99S9d9J/tmGIalv2pUrFjRoyeOC+SsDDri4uIy/D3H +38/7jBkzVK5cOfXp00enT5+2/LlXr17VmDFjVKpUKb399tumR9MREQVzBw4c0Jw5c0zX1ahRwyMn +vmYAQESW27t3r+maunXrBq2vZcuW6X4sISFBzz33nBITE93a9q+//qrXXnst3Y/ny5dPDRs2DJp9 +lSNHDr344oum6xISEtSzZ0++eDJxmzdv1vHjx03XZYbD/9Oy+gPcjz/+GJS+pUuXqnr16oqOjnY6 +9DTr3LlzGjhwoO666y599tlnSk1N5QuKiGzXqFGjLJ341ZN//WcAQESWfpGzcjimlWvEB2r169dX +uXLl0v34L7/8olatWlk+jPXGz2vatKnTv2K98sorbl/BwEqHDx/WuHHjPLrNvn37KmvWrKbr5s+f +r3nz5vFFlEmzeig7AwD3912gtH37djVu3FiPPPKINm3a5NHvX+3bt9c999xj6WgSIqJg6eTJk/rk +k09M15UpU0ZPP/00AwAi8l0XL160tM7Zde4DPYfDocGDBztdM2/ePNWqVcvS2yESEhI0cuRINWjQ +QKdOnUp3Xa5cubxy3gTDMPTTTz/p6aefVsmSJdWjRw/98ssvHtt+kSJF1LlzZ0trO3Xq5PQSiGTf +VqxYYWldMJ0DI6Pdeeedlt4qtXz5co9dgcSbHTlyRDExMapatarpUQvPPfdcuv9OmA1Ct27dqsce +e0wNGzbU5s2b+eIioqBv3LhxunLlium6fv36efwy3wwAiMhpVs7gLcnlv44HWm3atFGdOnWcrtm+ +fbtq1aqlRo0aacqUKdqxY4dOnz6tpKSkfy7fNWjQIJUuXVr9+/fX1atXnW7vrbfe0m233eYxw7lz +5zRu3DiVL19eDRs21Ny5c/85tGz8+PEe3V//93//Z2noEx8frx49evCFlAn77bffTNfkypVLxYsX +z1T7pVKlSqZrzp49q/379wes4dy5c4qNjdVdd92lqVOnOj1Ev1y5clq6dKlmzpypbNmy3XLNG2+8 +oW3btpmeD+Wnn37SvffeqxdeeMHppVWJiAK5ixcvauLEiabrChUqpLZt23r+ARjkkw4cOGBIsnQb +PXo0O8zPRUVFWX6+gvlmpXPnzlna1ltvvWV5/27fvv2W27hy5Ypfn/c//vjDyJMnj0/2fZMmTYzU +1FSPPO4tW7YYr7zyihEREZHu/WXJksU4duyYR/fX1KlTLXtnzpzJN5ZM1N69ey29LmrXrp3p9k23 +bt0s7ZsZM2YE3GNPTEw0xowZY+TPn9/08efIkcMYPny4cfXq1X8+v0iRIrdcGx8f/8+aGTNmGLff +frvp9sPDw40+ffoYp0+fDvjnfMGCBaaekSNH8o2DKJP03nvvWfp3YMSIEV65f44AILpFe/bs0Zkz +Z1y6rVu3znS7a9eudWmbVk6+N2/ePJcfa9rNSjlz5kz3rzY324K9kiVLas6cOZbe356RqlSpoi++ ++EIOh8PtbSQlJWnWrFmqW7euqlatqg8++MDpURhJSUmaMmWKRx3t2rWz9NdMSerYsWOGTgpGwdXG +jRstrcuM13u3ara6D32RYRiaPXu2ypcvr549e5qeF6ZZs2batWuXBg4c6PI5Tlq3bq3du3erZ8+e +Cg0NTXddYmKiRo0apVKlSundd991+0StRES+LCkpSaNHjzZdlytXLnXs2NErjyGMp4Ho1l90rrZl +yxanH8+fP79q1qzp0i99y5cvd/4FHBamhx9+WBEREV7bFw6HQyVLltSuXbucrlu2bJmuXLmi7Nmz +B/Vz37BhQ82ePTtDZ/43++V/0aJFypMnj1uff+TIEb3//vv68MMPdeLECcufFxUVpdy5c3vMkZCQ +oI4dO2r79u2W1p87d05t2rTRqlWrFBbGPz12b8OGDQwAMmi2ug+93YoVK9S/f39Lj6dUqVIaP368 +GjdunKH7zJkzp0aPHq2YmBh17txZa9asSXftmTNn1K9fP02YMEHDhg1TdHR0hoarRETebPr06ZYu +9dqxY0eP/tx2YxwBQOShVq1a5fTjdevWdfmHktWrVzv9ePXq1b36y39a99xzj+maS5cuefxs8/6q +WbNmWrx4saKiojy63SeeeEKrVq1ya7vLli1T8+bNVaJECQ0bNszyL/916tTRjBkzdPjwYfXu3dsj +joMHD6p27dr67LPPXPo8s0sikn2y8v5/SZaPILFTFStWtHRCp02bNvn18nfHjh3TE088oQYNGpj+ +8p8tWzYNGTJEcXFxGf7l/8YqV66s1atX65NPPlHBggWdrj106JBeeOEF3XvvvZZff0REvswwDI0c +OdJ0XdasWb16KWUGAEQ+GgDUq1cvILbpTlYv0zVixIiAPnGVK9WrV09bt25VixYtMryt3Llza/z4 +8Zo/f75LR5ecP39e48ePV/ny5fXwww/rm2++sXS92MjISL366qvatm2bVq9erdatW3vsUoO//PKL +qlev7valvkaOHBm01zgn6+3cudPSuhIlSmS6fZMjRw5LJ/+8dOmS/vzzT789zty5c5se2SZJjRs3 +VlxcnF5//XVLbxdzNYfDoZiYGO3evVsdO3Y0HZ7ExcX5ZDBORORqCxYssPTvY9u2bb16eW0GAD7K +lR++vXlNcPJOBw4cMD2c58EHH3RpmxcvXjT94cvVbbrb008/bel1efbsWTVu3FjHjx+3xfMaFRWl +OXPmaNWqVXr88cddvgxL3rx5NWDAAO3Zs0ddu3a1fATI9u3b1alTJxUpUkTdu3fX77//bunzypcv +r3Hjxuno0aOaMmWKx/+6unTpUj366KOm7/91lmEYatu2rY4dO8Y3Dpt2+fJlnTx50nRdaGioChUq +lCn3UZEiRSyt8+d5MyIiIvT222+n+/FixYrp66+/1vfff69SpUp5/fHkzZtXkydP1rp161S9evV0 +13Xt2lXlypXjC5GIAi5n31P/+eU8JER9+/b16uNgAOCjXPmrn7fe70Hea+XKlabPf5UqVVza5po1 +a5z+tTckJES1a9f2iS9//vx65plnLK3du3evqlWrpqVLl9rm+a1bt66+++47HT58WBMmTFDLli1V +rly5f/21K1++fKpZs6a6deumefPm6fjx4xoxYoSlv/YlJSVp9uzZevDBB1W5cmVNmTJFFy9eNP28 +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 +mTIZOgmfNypWrJimTp2qoUOHavz48fr888/1119/ef1+V69erZo1a+q1117Tf/7zH5/9Er1z505N +mjRJn3/+uemw5saqVaumTp06KTo62q33qRuGocuXLyssLMzSL9/uNGTIENMT8nXt2lX33HOP5W3W +qFFD//3vf9WvXz/TtfPmzdPYsWOdvrXArJAQ57Ntb+07utaVK1csrcvMgxirA4Dk5GQlJydzZCBR +gLZ06VJ9++23PrmvatWq3fKcOxSYffTRRzp16pTpuu7du/v83EX8i8IAgDJQZnz/v7PuuOMOjRw5 +UsOHD9fixYs1d+5cff/99zp69KjX7jM5OVlvvPGGfvnlF82ePVt58+b12v3MmzdPEydOdOna9eHh +4Xr22WfVuXNnPfDAA5Y/LyUlRStWrNCiRYu0du1a7d27VydOnPjnl/Ps2bOrVKlSqlatmh566CE1 +a9ZMefLkyZBx//79mj9/vukvbbGxsS5vu0+fPlq2bJl++OEH07X9+/dXnTp1XL56RlpXr151+vGM +7idyntWTnlo5Wsaumb1G03I4HPzyTxTAbd68WRMnTvTJfTVv3pwBQJCUnJys9957z3RdRESEunTp +4vPHx78qDADIzc6ePavt27c7XePOofo///yzx7fp828sYWF6/PHH9fjjj0uSdu/erTVr1mjt2rXa +tm2bduzYYemM+K60ZMkSPfjgg1q2bJkKFCjgse3Gx8frww8/1AcffODSIKN48eJ69dVX1aFDB5eu +cX/hwgVNnDhR48aNU3x8fLrrrly5ori4OMXFxWnatGnq2LGjWrZsqdjYWJUvX94tq5XL1MTExLh1 +ngKHw6HPPvtMVapUcepK++WoVatW2rRpk1snRTX75apQoUJ8A/NiERERltZZPVLAjlm1c0ULIqLg +a9asWaaXUZakl19+Wfny5fP54+McAAwAyM1Wr16t1NRUp78Eu/IXX0natWuXTp48GfQDgJsrW7as +XnzxRX3wwQdau3at187kv337dj3yyCMeed/s6tWr9dxzz6l48eIaMmSIpV/+HQ6HGjVqpPnz5+uP +P/5QbGysS7/8f/XVVypXrpxiY2NNf0m+ucTERE2bNk2VKlVSjx493NoHs2bNMl3Trl07t/dpwYIF +9cUXX5geoi9dOxrh1Vdfdet+zIZLgfYWGrtl9ZdWBgCZbwBw5MgRGYbxrxtDOSKyU1YubRwWFqZe +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 ++AAAIABJREFUVlZWOHHiBCUjIgiCIAiCIAiCIAgFgTnzlqamJg4dOoTatWsLtX337h2cnZ0REhIi +dpwzQRAEQRAEQRAEQRDiw+wBUEp8fDzat2+PxMREJnsXFxeEhobCwcGBRpsgCIIgCIIgCIIg5ATv +2lv16tXDpUuXUKtWLSb7W7duwdHRER4eHnj8+DGNOEEQBEEQBEEQBEEogwDwvQhgZWXFfMzRo0fR +tGlTdOjQATt37pRY6ZhSEhMTsX79eqHlCgmCIAiCIAiCIAiiIsI7BOB7Pn/+jAEDBuDSpUu8j61U +qRJ+/vlndOjQAe3bt0eTJk2go6PDfHxGRgZu3ryJ69ev49y5c7hz5w44jsP69esxYcIEurMEQRAE +QRAEQRAEISkBAPhWHWD69OlYu3atWB1RV1eHpaUlbG1tUatWrX9rI+rr6yM/Px85OTnIyMjAmzdv +EB8fj6SkJJTV9VatWuHGjRt0ZwmCIAiCIAiCIAhCkgJAKUePHsXEiRPx4cMHuV/UP//8gzp16tDd +JQiCIAiCIAiCIIj/R11SDfXt2xdxcXGYMGEC1NXV5XpRe/bsoTtLEARBEARBEARBEN8hMQ+A74mO +jsa8efNw8uRJSKF5oTRo0ABxcXF0dwmCIAiCIAiCIAji/5HKVr2joyOOHz+OmJgYDBo0CBoaGjK9 +qGfPnuH+/ft0dwmCIAiCIAiCIAhCmgJAKU2aNMHevXvx/v17rF69Gs7OzjK5KAcHB2RlZdHdJQiC +IAiCIAiCIIj/RyohAIL4559/cPr0aVy7dg3Xrl1DYmKi2G1qaWnByckJbm5uGDRoEOzs7OjOEgRB +EARBEARBEIQ8BYAfefPmDWJiYvDPP//g1atXePXqFRITE5GdnY2cnBzk5uYiNzcXWlpaMDQ0hKGh +ISwsLGBnZwc7Ozs4ODigVatW0NfXp7tJEARBEARBEARBEIoqABAEQRAEQRAEQRAEIX3UaQgIgiAI +giAIgiAIggQAgiAIgiAIgiAIgiBIACAIgiAIgiAIgiAIggQAgiAIgiAIgiAIgiBIACAIgiAIgiAI +giAIggQAgiAIgiAIgiAIgiBIACAIgiAIgiAIgiAIggQAgiAIgiAIgiAIgiBIACAIgiAIgiAIgiAI +EgAIgiAIgiAIgiAIgiABgCAIgiAIgiAIgiAIEgAIgiAIgiAIgiAIgiABgCAIgiAIgiAIgiAIEgAI +giAIgiAIgiAIgiABgCAIgiAIgiAIgiAIEgAIgiAIgiAIgiAIgiABgCAIgiAIgiAIgiBIACAIgiAI +giAIgiAIggQAgiAIgiAIgiAIgiBIACAIgiAIgiAIgiAIggQAgiAIgiAIgiAIgiBIACAIgiAIgiAI +giAIggQAgiAIgiAIgiAIgiBIACAIgiAIgiAIgiAIggQAgiAIgiAIgiAIgiABgCAIgiAIgiAIgiAI +EgAIgiAIgiAIgiAIgiABgCAIgiAIgiAIgiAIEgAIgiAIgiAIgiAIgiABgCAIgiAIgiBUDAsLC6ip +qUn87+HDhzS4CoSmpqZU7nNycjINLiG955aGQLoUFhbixo0buHPnDuLi4vDs2TMkJycjKysLWVlZ +KCkpgaGhIQwNDVG1alXUr18fDRs2hIODA9q3bw99fX0aRIIgCIIgCIIgCIIEAEWE4zj8/fff2LZt +G86dO4fMzEyB9mlpaUhLS8Pbt28RHR397//X0dFBu3btMGTIEAwaNAg6Ojo0uEpGdnY2oqOjcf/+ +fTx69AgJCQl4//490tLSkJubi7y8PKirq0NHRweGhoYwMzND9erVYWNjg7p168Le3h7NmjWDhYUF +DSZBEFIlJycHWVlZTLZaWlowNTVViH6npaXh69evTLYGBgYwMDCgm00QBEFUWNQ4juNYDIODgxEb +Gyv5DqipQVNTEzo6OtDV1YWRkRHMzc1hbm4Oa2trNGrUCCYmJkoxmCUlJdiyZQuWLVuG+Ph4ibZt +ZmaG8ePHw9/fX2ZeATNmzMCKFSsk3q6Ghsa/99vQ0BDVq1eHubk5rKys0KhRI9jb28PR0RGVKlVS +yh/Vp0+fsHv3bpw6dQpXr15l/jAVdv9bt26NNm3aoGPHjmjevDnU1SUXwbNgwQKEhYVViEnP2dkZ +R44cEWr3+PFjHDp0iKnNUaNGwcrKSmbzTHBwMJNt+/bt0b59eyZbKysrJCQkCLXT09NDenq6QgmS +rVu3xs2bN5ls379/j1q1agEATE1NkZaWJvSYw4cPw8PDQyme7/fv38PS0pLJ9sfX/+PHj9G8eXMU +FRUJPVZbWxuPHz+Gra2tXK/31atXsLe3R35+PtO75/bt23B0dPzPGLi4uODOnTtM59u8eTN+++03 +hbrnp06dQo8ePZhsTU1N8fLlS1SuXJnJvnPnzjh//rzSz/udOnXCuXPnZHpOCwsLJCYmSrzdBw8e +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 +""" diff --git a/glue/stimflow/src/stimflow/_viz/_3d_model_viewer.py b/glue/stimflow/src/stimflow/_viz/_3d_model_viewer.py new file mode 100644 index 00000000..ac68950f --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_3d_model_viewer.py @@ -0,0 +1,212 @@ +from __future__ import annotations + +import base64 + +import pygltflib + +from stimflow._core import str_html + + +class Viewable3dModelGLTF(pygltflib.GLTF2): + """A pygltflib.GLTF2 augmented with the ability to create a simple 3d viewer for the model.""" + + def html_viewer(self) -> str_html: + """Returns an HTML document that embeds the 3d model within a 3d viewer.""" + return html_viewer_for_gltf_model(self) + + def _repr_html_(self) -> str: + """This method causes Jupyter notebooks to show the model using an inline HTML viewer.""" + return self.html_viewer() + + +def html_viewer_for_gltf_model(model: pygltflib.GLTF2) -> str_html: + model_bytes = b"".join(model.save_to_bytes()) + + model_data_uri = f"""data:text/plain;base64,{base64.b64encode(model_bytes).decode()}""" + + return str_html( + r''' + + + + + + + Download 3D Model as .GLTF File +
Mouse Wheel = Zoom. Left Drag = Orbit. Right Drag = Strafe. +
+
JavaScript Blocked?
+
+ + + + """ # noqa: E501 + ) diff --git a/glue/stimflow/src/stimflow/_viz/__init__.py b/glue/stimflow/src/stimflow/_viz/__init__.py new file mode 100644 index 00000000..ef6ece12 --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/__init__.py @@ -0,0 +1,12 @@ +from stimflow._viz._3d_model_viewer import ( + Viewable3dModelGLTF, + html_viewer_for_gltf_model, +) +from stimflow._viz._viz_circuit_html import stim_circuit_html_viewer +from stimflow._viz._3d_model import ( + LineData, + TriangleData, + TextData, + make_3d_model, +) +from stimflow._viz._viz_svg import svg diff --git a/glue/stimflow/src/stimflow/_viz/_viz_circuit_html.py b/glue/stimflow/src/stimflow/_viz/_viz_circuit_html.py new file mode 100644 index 00000000..e688b375 --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_viz_circuit_html.py @@ -0,0 +1,1094 @@ +from __future__ import annotations + +import base64 +import collections +import dataclasses +import math +import random +import sys +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING + +import stim + +from stimflow._viz._viz_patch_svg import svg_path_directions_for_tile + +if TYPE_CHECKING: + import stimflow + from stimflow._core import str_html + + +PITCH = 48 * 2 +DIAM = 32 +RAD = DIAM / 2 +NOISY_GATES = { + "X_ERROR", + "Y_ERROR", + "Z_ERROR", + "E", + "ELSE_CORRELATED_ERROR", + "DEPOLARIZE1", + "DEPOLARIZE2", + "HERALDED_ERASE", + "HERALDED_PAULI_CHANNEL_1", + "PAULI_CHANNEL_1", + "PAULI_CHANNEL_2", + "I_ERROR", + "II_ERROR", +} + + +def rand_color() -> str: + color = "#" + for _ in range(6): + color += "0123456789abcdef"[random.randint(0, 15)] + return color + + +MEASUREMENT_NAMES = {"M", "MX", "MY", "MR", "MRX", "MRY"} + + +@dataclasses.dataclass +class GateStyle: + label: str + fill_color: str + text_color: str + + +def _init_gate_box_labels() -> dict[str, GateStyle]: + result = {"I": GateStyle(label="I", fill_color="white", text_color="gray")} + for name in ["X", "Y", "Z", "II", "I"]: + result[name] = GateStyle(label=name, fill_color="white", text_color="black") + for name in ["R", "M", "RX", "RY", "MX", "MY", "MR", "MRX", "MRY"]: + result[name] = GateStyle(label=name, fill_color="black", text_color="white") + for key in [ + "H", + "H_YZ", + "H_XY", + "S", + "SQRT_X", + "SQRT_Y", + "S_DAG", + "SQRT_X_DAG", + "SQRT_Y_DAG", + "H_NXY", + "H_NXZ", + "H_NYZ", + ]: + name = key.replace("SQRT_", "√") + name = name.replace("_DAG", "⁻¹") + a, b = name.split("_") if "_" in name else (name, "") + result[key] = GateStyle(label=a + b.lower(), fill_color="yellow", text_color="black") + for name in ["C_XYZ", "C_NXYZ", "C_XNYZ", "C_XYNZ", "C_ZYX", "C_NZYX", "C_ZNYX", "C_ZYNX"]: + result[name] = GateStyle( + label=name[0] + name[2:].lower(), fill_color="teal", text_color="black" + ) + return result + + +GATE_BOX_LABELS = _init_gate_box_labels() +TWO_QUBIT_GATE_STYLES = { + "CX": ("Z", "X"), + "CY": ("Z", "Y"), + "CZ": ("Z", "Z"), + "XCX": ("X", "X"), + "XCY": ("X", "Y"), + "XCZ": ("X", "Z"), + "YCX": ("Y", "X"), + "YCY": ("Y", "Y"), + "YCZ": ("Y", "Z"), + "SQRT_XX": ("SQRT_XX", "SQRT_XX"), + "SQRT_XX_DAG": ("SQRT_XX", "SQRT_XX"), + "SQRT_YY": ("SQRT_YY", "SQRT_YY"), + "SQRT_YY_DAG": ("SQRT_YY", "SQRT_YY"), + "SQRT_ZZ": ("SQRT_ZZ", "SQRT_ZZ"), + "SQRT_ZZ_DAG": ("SQRT_ZZ", "SQRT_ZZ"), + "ISWAP": ("ISWAP", "ISWAP"), + "ISWAP_DAG": ("ISWAP", "ISWAP"), + "SWAP": ("SWAP", "SWAP"), + "CXSWAP": ("ZSWAP", "XSWAP"), + "SWAPCX": ("XSWAP", "ZSWAP"), + "CZSWAP": ("ZSWAP", "ZSWAP"), + "MXX": ("MXX", "MXX"), + "MYY": ("MYY", "MYY"), + "MZZ": ("MZZ", "MZZ"), +} + + +def tag_str(tag, *, content: bool | str = False, **kwargs) -> str: + parts = [f"<{tag}"] + for k, v in kwargs.items(): + parts.append(f"{k.replace('_', '-')}={str(v)!r}") + instr = " ".join(parts) + if not content: + instr += " />" + elif isinstance(content, str): + instr += f">{content}" + elif content is True: + instr += ">" + else: + raise NotImplementedError(repr(content)) + + return instr + + +class _SvgLayer: + def __init__( + self, + patch: stimflow.Patch, + tile_color_func: Callable[ + [stimflow.Tile], tuple[float, float, float, float] | tuple[float, float, float] | str + ], + ): + self.svg_instructions: list[str] = [] + self.q2i_dict: dict[int, tuple[float, float]] = {} + self.used_indices: set[int] = set() + self.used_positions: set[tuple[float, float]] = set() + self.measurement_positions: dict[int, tuple[float, float]] = {} + if patch is not None: + self.add_patch(patch, tile_color_func=tile_color_func) + + def add(self, tag, *, content: bool | str = False, **kwargs) -> None: + self.svg_instructions.append(" " + tag_str(tag, content=content, **kwargs)) + + def bounds(self) -> tuple[float, float, float, float]: + min_y = min([e for _, e in self.used_positions], default=0) + max_y = max([e for _, e in self.used_positions], default=0) + min_x = min([e for e, _ in self.used_positions], default=0) + max_x = max([e for e, _ in self.used_positions], default=0) + min_x -= PITCH + min_y -= PITCH + max_x += PITCH + max_y += PITCH + return min_x, min_y, max_x, max_y + + def add_idles(self, all_used_positions: set[tuple[float, float]]): + for x, y in all_used_positions - self.used_positions: + self.add("circle", cx=x, cy=y, r=5, fill="gray", stroke="black") + self.used_positions |= all_used_positions + min_x, _min_y, _max_x, max_y = self.bounds() + xs = {e for e, _ in self.used_positions} + ys = {e for _, e in self.used_positions} + for x in xs: + x2 = x + x2 /= PITCH + if x2 == int(x2): + x2 = int(x2) + self.add( + "text", + x=x, + y=max_y - 5, + fill="black", + content=str(x2), + text_anchor="middle", + dominant_baseline="auto", + font_size=24, + ) + for y in ys: + y2 = y + y2 /= PITCH + if y2 == int(y2): + y2 = int(y2) + self.add( + "text", + x=min_x + 5, + y=y, + fill="black", + content=str(y2), + text_anchor="left", + alignment_baseline="middle", + font_size=24, + ) + + def add_patch( + self, + patch: stimflow.Patch, + tile_color_func: Callable[ + [stimflow.Tile], tuple[float, float, float, float] | tuple[float, float, float] | str + ], + ): + for tile in patch.tiles: + color = tile_color_func(tile) + if isinstance(color, tuple): + if len(color) == 3: + r, g, b = color + a = 0.3 + elif len(color) == 4: + r, g, b, a = color + else: + raise NotImplementedError(f"{color=}") + assert 0 <= r <= 1 + assert 0 <= g <= 1 + assert 0 <= b <= 1 + assert 0 <= a <= 1 + color = ( + "#" + + f"{round(r * 255.49):x}".rjust(2, "0") + + f"{round(g * 255.49):x}".rjust(2, "0") + + f"{round(b * 255.49):x}".rjust(2, "0") + + f"{round(a * 255.49):x}".rjust(2, "0") + ) + + path_directions = svg_path_directions_for_tile( + tile=tile, draw_coord=lambda pt: pt * PITCH + ) + if path_directions is not None: + self.svg_instructions.append( + f"""""" + ) + for tile in patch.tiles: + path_directions = svg_path_directions_for_tile( + tile=tile, draw_coord=lambda pt: pt * PITCH + ) + if path_directions is not None: + self.svg_instructions.append( + f'' + ) + + def svg( + self, + *, + html_id: str | None = None, + as_img_with_data_uri: bool = False, + width: int, + height: int, + ) -> str: + min_x, min_y, max_x, max_y = self.bounds() + kwargs = {} if html_id is None or as_img_with_data_uri else {"id": html_id} + svg = "\n".join( + [ + tag_str( + "svg", + xmlns="http://www.w3.org/2000/svg", + viewBox=f"{min_x} {min_y} {max_x - min_x} {max_y - min_y}", + content=True, + **kwargs, + ), + *self.svg_instructions, + "", + ] + ) + if as_img_with_data_uri: + kwargs = {} if html_id is None else {"id": html_id} + svg = tag_str( + "img", + width=width, + height=height, + **kwargs, + src="data:image/svg+xml;base64," + + base64.standard_b64encode(svg.encode("utf-8")).decode("utf-8"), + ) + svg = svg.replace("/>", ">") + return svg + + +class _SvgState: + def __init__( + self, + patch: stimflow.Patch | None, + tile_color_func: Callable[ + [stimflow.Tile], tuple[float, float, float, float] | tuple[float, float, float] | str + ], + ): + self.patch: stimflow.Patch | None = patch + self.layers: list[_SvgLayer] = [_SvgLayer(self.patch, tile_color_func=tile_color_func)] + self.coord_shift: list[int] = [0, 0] + self.measurement_layer_indices: list[int] = [] + self.detector_index: int = 0 + self.detector_coords: dict[int, list[float]] = {} + self.measurement_marks: collections.Counter[int] = collections.Counter() + self.highlighted_detectors: set[int] = set() + self.highlighted_errors: list[tuple[int, int, str]] = [] + self.flipped_measurements: set[int] = set() + self.noted_errors: list[tuple[int, int, str]] = [] + self.control_count: int = 0 + self.tile_color_func = tile_color_func + + def tick(self) -> None: + self.layers.append(_SvgLayer(self.patch, self.tile_color_func)) + self.layers[-1].q2i_dict = dict(self.layers[-2].q2i_dict) + + def i2xy(self, i: int) -> tuple[float, float]: + x, y = self.layers[-1].q2i_dict.setdefault(i, (i, 0)) + pt = x * PITCH, y * PITCH + self.layers[-1].used_indices.add(i) + self.layers[-1].used_positions.add(pt) + return pt + + def are_adjacent(self, q1: stim.GateTarget, q2: stim.GateTarget) -> bool: + if q1.is_qubit_target and q2.is_qubit_target: + x1, y1 = self.layers[-1].q2i_dict.setdefault(q1.value, (q1.value, 0)) + x2, y2 = self.layers[-1].q2i_dict.setdefault(q2.value, (q2.value, 0)) + if abs(x2 - x1) + abs(y2 - y1) < 1.5: + return True + return False + + def add(self, tag, *, content="", **kwargs) -> None: + self.layers[-1].add(tag, content=content, **kwargs) + + def add_box(self, x: float, y: float, text: str, *, fill="white", text_color="black"): + self.add("rect", x=x - RAD, y=y - RAD, width=DIAM, height=DIAM, fill=fill, stroke="black") + self.add( + "text", + x=x, + y=y, + fill=text_color, + content=text, + font_size=32 if len(text) == 1 else 24 if len(text) == 2 else 18, + text_anchor="middle", + alignment_baseline="central", + ) + + def add_measurement(self, *targets: stim.GateTarget) -> None: + for target in targets: + assert ( + target.is_qubit_target + or target.is_x_target + or target.is_y_target + or target.is_z_target + ) + m_index = len(self.measurement_layer_indices) + self.measurement_layer_indices.append(len(self.layers) - 1) + x: float = 0 + y: float = 0 + for target in targets: + dx, dy = self.i2xy(target.value) + x += dx + y += dy + x /= len(targets) + y /= len(targets) + self.layers[-1].measurement_positions[m_index] = (x, y) + + def mark_measurements( + self, targets: list[stim.GateTarget], prefix: str, index: int | None + ) -> None: + if index is None: + assert prefix == "D" + index = self.detector_index + self.detector_index += 1 + if prefix == "D": + color = "black" + if index in self.highlighted_detectors: + color = "red" + elif prefix == "L": + color = "blue" + elif prefix == "C": + color = "green" + else: + color = "black" + name = f"{prefix}{index}" + for t in targets: + m_index = len(self.measurement_layer_indices) + t.value + if m_index < 0: + print( + "Attempted to mark a measurement before the beginning of time.\n" + "Skipping this mark.", + file=sys.stderr, + ) + continue + assert m_index >= 0, m_index + if t.is_measurement_record_target: + layer = self.layers[self.measurement_layer_indices[m_index]] + x, y = layer.measurement_positions[m_index] + x += RAD + 1 + y -= RAD + y += self.measurement_marks[m_index] * 15 + self.measurement_marks[m_index] += 1 + layer.add( + "text", + x=x, + y=y, + fill=color, + content=name, + text_anchor="left", + alignment_baseline="hanging", + font_size=16, + ) + + +def _draw_endpoint(x: float, y: float, style: str, *, out: _SvgState) -> None: + add = out.add + if style == "X": + add("circle", cx=x, cy=y, r=RAD, stroke="black", fill="white") + add("line", x1=x - RAD, x2=x + RAD, y1=y, y2=y, stroke="black") + add("line", x1=x, x2=x, y1=y - RAD, y2=y + RAD, stroke="black") + elif style == "Y": + s = 0.5**0.5 + add("circle", cx=x, cy=y, r=RAD, stroke="black", fill="white") + add("line", x1=x, x2=x, y1=y, y2=y + RAD, stroke="black") + add("line", x1=x, x2=x - RAD * s, y1=y, y2=y - RAD * s, stroke="black") + add("line", x1=x, x2=x + RAD * s, y1=y, y2=y - RAD * s, stroke="black") + elif style == "Z": + add("circle", cx=x, cy=y, r=RAD, fill="black") + elif style == "SWAP": + r = RAD / 3 + add("line", x1=x - r, x2=x + r, y1=y - r, y2=y + r, stroke="black") + add("line", x1=x - r, x2=x + r, y1=y + r, y2=y - r, stroke="black") + elif style == "ISWAP": + r = RAD + add("circle", cx=x, cy=y, r=RAD / 2, fill="gray") + add("line", x1=x - r, x2=x + r, y1=y - r, y2=y + r, stroke="black") + add("line", x1=x - r, x2=x + r, y1=y + r, y2=y - r, stroke="black") + elif style == "MXX": + out.add_box(x=x, y=y, text="Mxx", fill="black", text_color="white") + elif style == "MYY": + out.add_box(x=x, y=y, text="Myy", fill="black", text_color="white") + elif style == "MZZ": + out.add_box(x=x, y=y, text="Mzz", fill="black", text_color="white") + elif style == "SQRT_ZZ": + out.add_box(x=x, y=y, text="√ZZ") + elif style == "SQRT_YY": + out.add_box(x=x, y=y, text="√YY") + elif style == "SQRT_XX": + out.add_box(x=x, y=y, text="√XX") + elif style == "XSWAP": + r = RAD * 0.4 + add("circle", cx=x, cy=y, r=RAD, fill="white", stroke="black") + add("line", x1=x - r, x2=x + r, y1=y - r, y2=y + r, stroke="black", stroke_width=5) + add("line", x1=x - r, x2=x + r, y1=y + r, y2=y - r, stroke="black", stroke_width=5) + elif style == "ZSWAP": + r = RAD * 0.4 + add("circle", cx=x, cy=y, r=RAD, fill="black", stroke="black") + add("line", x1=x - r, x2=x + r, y1=y - r, y2=y + r, stroke="white", stroke_width=5) + add("line", x1=x - r, x2=x + r, y1=y + r, y2=y - r, stroke="white", stroke_width=5) + else: + raise NotImplementedError(style) + + +def _draw_2q(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: + style1, style2 = TWO_QUBIT_GATE_STYLES[instruction.name] + targets = instruction.targets_copy() + i2qq = out.i2xy + is_measurement = stim.gate_data(instruction.name).produces_measurements + + assert len(targets) % 2 == 0 + for k in range(0, len(targets), 2): + t1 = targets[k] + t2 = targets[k + 1] + if is_measurement: + out.add_measurement(t1, t2) + if t1.is_measurement_record_target or t2.is_measurement_record_target: + if t1.is_qubit_target: + t = t1.value + m = t2 + elif t2.is_qubit_target: + t = t2.value + m = t1 + else: + continue + b = ( + "X" + if instruction.name in ["XCZ", "CX"] + else ( + "Y" + if instruction.name in ["YCZ", "CY"] + else "Z" if instruction.name == "CZ" else "?" + ) + ) + x, y = i2qq(t) + out.add( + "text", + x=x - RAD + 1, + y=y, + fill="green", + content=b, + font_size=18, + text_anchor="left", + alignment_baseline="central", + ) + out.add( + "text", + x=x - 1, + y=y - RAD / 2, + fill="green", + content=f"C{out.control_count}", + font_size=8, + text_anchor="left", + alignment_baseline="central", + ) + out.mark_measurements([m], prefix="C", index=out.control_count) + out.control_count += 1 + continue + assert t1.is_qubit_target + assert t2.is_qubit_target + x1, y1 = i2qq(t1.value) + x2, y2 = i2qq(t2.value) + dx = x2 - x1 + dy = y2 - y1 + r = (dx * dx + dy * dy) ** 0.5 + px = dy + py = -dx + px *= 25 / r + py *= 25 / r + cx1 = dx / 10 + px + cy1 = dy / 10 + py + cx2 = dx - dx / 10 + px + cy2 = dy - dy / 10 + py + + if out.are_adjacent(t1, t2): + out.add("line", x1=x1, x2=x2, y1=y1, y2=y2, stroke="black") + else: + out.add( + "path", + d=f"M {x1},{y1} c {cx1},{cy1} {cx2},{cy2} {dx},{dy}", + stroke="black", + fill="none", + ) + + _draw_endpoint(x1, y1, style1, out=out) + _draw_endpoint(x2, y2, style2, out=out) + + +def _draw_mpp(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: + for chunk in instruction.target_groups(): + out.add_measurement(*chunk) + _draw_single_mpp(chunk, out=out, tag=instruction.tag, include_qubit_boxes=True) + + +def _draw_spp(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: + for chunk in instruction.target_groups(): + out.add_measurement(*chunk) + _draw_single_spp(chunk, out=out, tag=instruction.tag, include_qubit_boxes=True) + + +def _draw_single_spp( + chunk: list[stim.GateTarget], *, out: _SvgState, tag: str, include_qubit_boxes: bool +) -> None: + tx, ty = 0.0, 0.0 + for t in chunk: + x, y = out.i2xy(t.value) + tx += x + ty += y + tx /= len(chunk) + ty /= len(chunk) + if tag: + out.add_box(tx, ty, tag) + color = rand_color() + no_text = False + if all(t.is_x_target for t in chunk): + color = "red" + no_text = True + if all(t.is_y_target for t in chunk): + color = "green" + no_text = True + if all(t.is_z_target for t in chunk): + color = "blue" + no_text = True + for t in chunk: + x, y = out.i2xy(t.value) + out.add("line", x1=x, x2=tx, y1=y, y2=ty, stroke=color, stroke_width=8) + if include_qubit_boxes: + for c in chunk: + if c.is_x_target: + text = "SX" + elif c.is_y_target: + text = "SY" + elif c.is_z_target: + text = "SZ" + else: + raise NotImplementedError(repr(c)) + x, y = out.i2xy(c.value) + out.add_box(x, y, text * (1 - int(no_text)), fill=color) + + +def _draw_single_mpp( + chunk: list[stim.GateTarget], *, out: _SvgState, tag: str, include_qubit_boxes: bool +) -> None: + add = out.add + add_box = out.add_box + q2i = out.i2xy + + tx, ty = 0.0, 0.0 + for t in chunk: + x, y = q2i(t.value) + tx += x + ty += y + tx /= len(chunk) + ty /= len(chunk) + if tag: + add_box(tx, ty, tag) + color = rand_color() + no_text = False + if all(t.is_x_target for t in chunk): + color = "red" + no_text = True + if all(t.is_y_target for t in chunk): + color = "green" + no_text = True + if all(t.is_z_target for t in chunk): + color = "blue" + no_text = True + for t in chunk: + x, y = q2i(t.value) + add("line", x1=x, x2=tx, y1=y, y2=ty, stroke=color, stroke_width=8) + if include_qubit_boxes: + for c in chunk: + if c.is_x_target: + text = "PX" + elif c.is_y_target: + text = "PY" + elif c.is_z_target: + text = "PZ" + else: + raise NotImplementedError(repr(c)) + x, y = q2i(c.value) + add_box(x, y, text * (1 - int(no_text)), fill=color) + + +def _draw_1q(instruction: stim.CircuitInstruction, *, out: _SvgState): + targets = instruction.targets_copy() + if instruction.name in MEASUREMENT_NAMES: + for t in targets: + out.add_measurement(t) + for t in targets: + assert t.is_qubit_target + x, y = out.i2xy(t.value) + style = GATE_BOX_LABELS[instruction.name] + out.add_box(x, y, style.label, fill=style.fill_color, text_color=style.text_color) + + +def _stim_circuit_to_svg_helper(circuit: stim.Circuit, state: _SvgState) -> None: + for instruction in circuit: + if isinstance(instruction, stim.CircuitRepeatBlock): + body = instruction.body_copy() + for _ in range(instruction.repeat_count): + _stim_circuit_to_svg_helper(body, state) + elif isinstance(instruction, stim.CircuitInstruction): + targets: list[stim.GateTarget] = instruction.targets_copy() + if instruction.name == "QUBIT_COORDS": + pos = instruction.gate_args_copy() + for t in instruction.targets_copy(): + assert t.is_qubit_target + if len(pos): + if len(pos) == 1: + pos = (pos[0], 0) + state.layers[-1].q2i_dict[t.value] = ( + pos[0] + state.coord_shift[0], + pos[1] + state.coord_shift[1], + ) + elif instruction.name == "SHIFT_COORDS": + pos = instruction.gate_args_copy() + if len(pos) >= 1: + state.coord_shift[0] += pos[0] + if len(pos) >= 2: + state.coord_shift[1] += pos[1] + elif instruction.name in GATE_BOX_LABELS: + _draw_1q(instruction, out=state) + elif instruction.name in TWO_QUBIT_GATE_STYLES: + _draw_2q(instruction, out=state) + elif instruction.name == "TICK": + state.tick() + elif instruction.name == "MPP": + _draw_mpp(instruction, out=state) + elif instruction.name == "SPP" or instruction.name == "SPP_DAG": + _draw_spp(instruction, out=state) + elif instruction.name == "DETECTOR": + state.mark_measurements(targets, prefix="D", index=None) + elif instruction.name == "OBSERVABLE_INCLUDE": + paulis = [t for t in targets if t.pauli_type != "I"] + if paulis: + _draw_single_mpp( + paulis, out=state, tag=instruction.tag, include_qubit_boxes=True + ) + state.mark_measurements( + targets, prefix="L", index=int(instruction.gate_args_copy()[0]) + ) + elif instruction.name == "E": + _draw_single_mpp( + instruction.targets_copy(), + out=state, + tag=instruction.tag, + include_qubit_boxes=False, + ) + elif instruction.name in NOISY_GATES: + for t in instruction.targets_copy(): + state.noted_errors.append((t.value, len(state.layers) - 1, "E")) + elif instruction.name == "MPAD": + for t in instruction.targets_copy(): + state.add_measurement(t) + else: + raise NotImplementedError(repr(instruction)) + else: + raise NotImplementedError(repr(instruction)) + + +def append_patch_polygons( + *, + out: list[str], + patch: stimflow.Patch, + q2i: dict[complex, int], + tile_color_func: Callable[ + [stimflow.Tile], tuple[float, float, float, float] | tuple[float, float, float] | str + ], +): + for e in patch.tiles: + rgba = tile_color_func(e) + if isinstance(rgba, str): + raise NotImplementedError(f"{rgba=}") + elif len(rgba) == 3: + r, g, b = rgba + a = 0.25 + assert 0 <= r <= 1 + assert 0 <= g <= 1 + assert 0 <= b <= 1 + elif len(rgba) == 4: + r, g, b, a = rgba + assert 0 <= r <= 1 + assert 0 <= g <= 1 + assert 0 <= b <= 1 + assert 0 <= a <= 1 + else: + raise NotImplementedError(f"{rgba=}") + qs = [q for q in e.data_qubits if q is not None] + c = e.measure_qubit + if c is None or any(abs(q - c) < 1e-4 for q in e.data_set): + c = sum(e.data_set) / len(e.data_set) + qs = sorted(qs, key=lambda q: math.atan2(q.imag - c.imag, q.real - c.real)) + line = f"POLYGON({r},{g},{b},{a})" + for q in qs: + line += f"_{q2i.get(q, 0)}" + out.append(line) + + +def stim_circuit_html_viewer( + circuit: stim.Circuit, + *, + background: ( + stimflow.Patch + | stimflow.StabilizerCode + | stimflow.ChunkInterface + | dict[int, stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface] + | None + ) = None, + tile_color_func: ( + Callable[[stimflow.Tile], tuple[float, float, float, float] | tuple[float, float, float] | str] + | None + ) = None, + width: int = 500, + height: int = 500, + known_error: Iterable[stim.ExplainedError] | None = None, +) -> str_html: + q2i = {} + for k, v in circuit.get_final_qubit_coordinates().items(): + if len(v) == 1: + q2i[v[0]] = k + elif len(v) >= 2: + q2i[v[0] + 1j * v[1]] = k + min_imag = min([q.imag for q in q2i.keys()], default=0) + seen_qubit_indices = set(q2i.values()) + for q in range(circuit.num_qubits): + if q not in seen_qubit_indices: + q2i[min_imag * 1j - 1j + q] = q + + if tile_color_func is None: + + def default_tile_color_func(tile: stimflow.Tile) -> tuple[float, float, float, float]: + if tile.basis == "X": + return 1, 0, 0, 0.25 + elif tile.basis == "Y": + return 0, 1, 0, 0.25 + elif tile.basis == "Z": + return 0, 0, 1, 0.25 + else: + return 0.5, 0.5, 0.5, 0.5 + + tile_color_func = default_tile_color_func + + from stimflow._chunk import ChunkInterface, find_d2_error, Patch, StabilizerCode + + if isinstance(background, StabilizerCode): + background = background.stabilizers + if isinstance(background, ChunkInterface): + background = background.to_patch() + background: stimflow.Patch | None + if isinstance(background, Patch): + background = background + elif isinstance(background, dict) and background: + val = background[min(background.keys(), key=lambda e: (e < 0, e))] + if isinstance(val, Patch): + background = val + elif isinstance(val, StabilizerCode): + background = val.patch + else: + raise NotImplementedError(f"{val=}") + else: + background = None + state = _SvgState(background, tile_color_func=tile_color_func) + state.detector_coords = circuit.get_detector_coordinates() + if known_error is None: + # noinspection PyBroadException + try: + known_error = find_d2_error(circuit) + if known_error is None: + known_error = circuit.shortest_graphlike_error( + ignore_ungraphlike_errors=True, canonicalize_circuit_errors=True + ) + except Exception: + pass + tick_highlights = {} + if known_error is not None: + for product in known_error: + loc = next(iter(product.circuit_error_locations)) + for flipped in loc.flipped_pauli_product: + if flipped.gate_target.is_x_target: + b = "X" + elif flipped.gate_target.is_y_target: + b = "Y" + elif flipped.gate_target.is_z_target: + b = "Z" + else: + raise NotImplementedError(repr(loc)) + tick_highlights[loc.tick_offset] = "red" + state.highlighted_errors.append((flipped.gate_target.value, loc.tick_offset, b)) + if loc.flipped_measurement is not None: + state.flipped_measurements.add(loc.flipped_measurement.record_index) + for term in product.dem_error_terms: + target = term.dem_target + if target.is_relative_detector_id(): + state.highlighted_detectors.add(target.val) + + _stim_circuit_to_svg_helper(circuit, state) + for t, layer in enumerate(state.layers): + if layer.measurement_positions: + if t not in tick_highlights: + tick_highlights[t] = "gray" + + all_pos = {pt for layer in state.layers for pt in layer.used_positions} + for layer in state.layers: + layer.add_idles(all_pos) + + for m in state.flipped_measurements: + layer = state.layers[state.measurement_layer_indices[m]] + x, y = layer.measurement_positions[m] + layer.add( + "rect", + x=x - RAD * 2, + y=y - RAD * 2, + width=DIAM * 2, + height=DIAM * 2, + fill="#FF000080", + stroke="#FF0000", + ) + for qubit, time, basis in state.highlighted_errors: + layer = state.layers[time] + x, y = state.i2xy(qubit) + layer.add( + "text", + x=x, + y=y, + fill="red", + content="," + basis, + text_anchor="middle", + dominant_baseline="middle", + font_size=64, + ) + for qubit, time, basis in set(state.noted_errors): + if time >= len(state.layers): + print(f"Error time is past end of circuit: {time}", file=sys.stderr) + continue + layer = state.layers[time] + x, y = state.i2xy(qubit) + layer.add( + "text", + x=x - RAD, + y=y, + fill="red", + content=basis, + text_anchor="end", + dominant_baseline="middle", + font_size=12, + ) + + # Draw the scrubber. + for t, layer in enumerate(state.layers): + min_x, min_y, max_x, _ = layer.bounds() + dx = (max_x - min_x) / len(state.layers) + layer.add( + "rect", x=min_x, y=min_y, width=max_x - min_x, height=10, fill="white", stroke="none" + ) + for t2, color in tick_highlights.items(): + layer.add( + "rect", x=min_x + t2 * dx, y=min_y, width=dx, height=10, fill=color, stroke="none" + ) + layer.add( + "rect", + x=min_x + (t + 0.25) * dx, + y=min_y, + width=dx * 0.5, + height=10, + fill="green", + stroke="none", + ) + layer.add( + "rect", x=min_x, y=min_y, width=max_x - min_x, height=10, fill="none", stroke="black" + ) + + svg_image_tags = [] + for k, layer in enumerate(state.layers): + svg = layer.svg(html_id=f"layer{k}", width=width, height=height) + data = base64.standard_b64encode(svg.encode("utf-8")).decode("utf-8") + svg_image_tags.append( + f'' + ) + all_svg_image_tags = "\n".join(svg_image_tags) + + flattened = circuit.flattened() + circuit_coords = [str(inst) for inst in flattened if inst.name == "QUBIT_COORDS"] + from stimflow._chunk import Patch + + i2patch: dict[int, Patch] + if isinstance(background, Patch): + i2patch = {0: background} + elif background is None: + i2patch = {} + elif isinstance(background, dict): + num_ticks = circuit.num_ticks + len(background) + i2patch = {} + for k, v in background.items(): + if k < 0: + k += num_ticks + 1 + if isinstance(v, StabilizerCode): + i2patch[k] = v.patch + elif isinstance(v, ChunkInterface): + i2patch[k] = v.to_patch() + elif isinstance(v, Patch): + i2patch[k] = v + else: + raise NotImplementedError(f"{v=}") + else: + raise NotImplementedError(f"{background=}") + tick = 0 + circuit_rest: list[str] = [] + for inst in flattened: + if tick in i2patch: + append_patch_polygons( + out=circuit_rest, patch=i2patch[tick], q2i=q2i, tile_color_func=tile_color_func + ) + circuit_rest.append("TICK") + tick += 1 + if inst.name == "TICK": + tick += 1 + if inst.name != "QUBIT_COORDS": + circuit_rest.append(str(inst)) + max_patch_tick = max(i2patch.keys(), default=0) + while tick <= max_patch_tick: + if tick in i2patch: + circuit_rest.append("TICK") + append_patch_polygons( + out=circuit_rest, patch=i2patch[tick], q2i=q2i, tile_color_func=tile_color_func + ) + tick += 1 + + escaped = ";".join(circuit_coords + circuit_rest) + escaped = escaped.replace(", ", ",").replace(" ", "_") + escaped = escaped.replace("QUBIT_COORDS", "Q") + escaped = escaped.replace("DETECTOR", "DT") + escaped = escaped.replace("(", "%28").replace(")", "%29") + escaped = escaped.replace("[", "%5B").replace("]", "%5D") + local_server_crumble_url = f"""https://algassert.com/crumble#circuit={escaped}""" + + from stimflow._core import str_html + + return str_html( + f""" + + + + + + +
+
Loading...
+ + + Open in Crumble +
""" + + all_svg_image_tags + + """ +
+ +
+""" + ) diff --git a/glue/stimflow/src/stimflow/_viz/_viz_circuit_html_test.py b/glue/stimflow/src/stimflow/_viz/_viz_circuit_html_test.py new file mode 100644 index 00000000..c3dec8f9 --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_viz_circuit_html_test.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import stim + +import stimflow + + +def test_viewer_works_with_all_gates(): + circuit = stim.Circuit( + """ + M 0 1 + """ + ) + for name, data in stim.gate_data().items(): + args = [0.01] * data.num_parens_arguments_range[0] + if name == "REPEAT": + continue + if name in ["DETECTOR", "OBSERVABLE_INCLUDE"]: + targets = [stim.target_rec(-1)] + args = [0] + elif name in ["SHIFT_COORDS", "TICK"]: + targets = [] + elif data.takes_pauli_targets: + targets = [stim.target_x(0)] + else: + targets = [0, 1] + circuit.append(name, targets, args) + viewer = stimflow.stim_circuit_html_viewer(circuit) + assert viewer is not None diff --git a/glue/stimflow/src/stimflow/_viz/_viz_circuit_layer_svg.py b/glue/stimflow/src/stimflow/_viz/_viz_circuit_layer_svg.py new file mode 100644 index 00000000..49833643 --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_viz_circuit_layer_svg.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable + +import stim + +from stimflow._core import str_svg + + +def append_circuit_layer_to_svg( + *, circuit: stim.Circuit, lines: list[str], q2p: Callable[[complex], complex] +): + i2q = {i: v[0] + v[1] * 1j for i, v in circuit.get_final_qubit_coordinates().items()} + scale = abs(q2p(1) - q2p(0)) + uses = collections.defaultdict(list) + + def t2p(t: stim.GateTarget) -> complex: + i = t.qubit_value + q = i2q[i] + return q2p(q) + + def slot1(t: stim.GateTarget) -> complex: + p = t2p(t) + uses[t].append(time) + offset = len(uses[t]) - 1.0 + offset *= r * 1.7 + return p + offset + + def slot2(t1: stim.GateTarget, t2: stim.GateTarget) -> tuple[complex, complex]: + p1 = t2p(t1) + p2 = t2p(t2) + key = frozenset([t1, t2]) + uses[key].append(time) + offset = len(uses[key]) - 1.0 + offset *= r * 0.8 + return p1 + offset, p2 + offset + + time = 0 + r = scale * 0.08 + sw = scale * 0.01 + fs = scale * 0.1 + for q in i2q.values(): + p = q2p(q) + lines.append( + f"""""" + ) + for instruction in circuit.flattened(): + if instruction.name == "TICK": + time += 1 + elif instruction.name == "RX" or instruction.name == "MX": + for t in instruction.targets_copy(): + p = slot1(t) + lines.append( + f"""""" + ) + lines.append( + f"""{instruction.name}""" + ) + elif instruction.name == "R" or instruction.name == "M": + for t in instruction.targets_copy(): + p = slot1(t) + lines.append( + f"""""" + ) + lines.append( + f"""{instruction.name}Z""" + ) + elif instruction.name == "CX" or instruction.name == "CZ": + for a, b in instruction.target_groups(): + pa, pb = slot2(a, b) + pc = (pa + pb) / 2 + pa = pa * 0.4 + pc * 0.6 + pb = pb * 0.4 + pc * 0.6 + lines.append( + f"""""" + ) + lines.append( + f"""""" + ) + if instruction.name == "CX": + lines.append( + f"""""" + ) + lines.append( + f"""""" + ) + elif instruction.name == "CZ": + lines.append( + f"""""" + ) + else: + raise NotImplementedError(f"{instruction=}") + elif instruction.name == "H": + for t in instruction.targets_copy(): + p = slot1(t) + lines.append( + f"""""" + ) + lines.append( + f"""H""" + ) + elif instruction.name == "QUBIT_COORDS": + pass + else: + raise NotImplementedError(f"{instruction=}") + for k, v in list(uses.items()): + label = ",".join(str(e) for e in v) + if isinstance(k, stim.GateTarget): + p = slot1(k) + p -= r * 0.5 + else: + a, b = k + pa, pb = slot2(a, b) + if pa.real > pb.real: + pa, pb = pb, pa + pc = (pa + pb) / 2 + p = pa + p += r * 0.7j * (1 if pb.imag > pa.imag else -1) + p = p * 0.5 + pc * 0.5 + p += 0.7 * r + lines.append( + f"""{label}""" + ) + return str_svg("\n".join(lines)) diff --git a/glue/stimflow/src/stimflow/_viz/_viz_patch_svg.py b/glue/stimflow/src/stimflow/_viz/_viz_patch_svg.py new file mode 100644 index 00000000..bd568b3f --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_viz_patch_svg.py @@ -0,0 +1,597 @@ +from __future__ import annotations + +import collections +import math +import sys +from collections.abc import Callable, Sequence +from typing import Any, Literal, TYPE_CHECKING + +import stim + +if TYPE_CHECKING: + import stimflow + + +def is_colinear(a: complex, b: complex, c: complex, *, atol: float = 1e-4) -> bool: + d1 = b - a + d2 = c - a + return abs(d1.real * d2.imag - d2.real * d1.imag) <= atol + + +def _path_commands_for_points_with_one_point( + *, a: complex, draw_coord: Callable[[complex], complex], draw_radius: float | None = None +): + draw_a = draw_coord(a) + if draw_radius is None: + draw_radius = abs(draw_coord(0.2) - draw_coord(0)) + r = draw_radius + left = draw_a - draw_radius + return [ + f"""M {left.real},{left.imag}""", + f"""a {r},{r} 0 0,0 {2*r},{0}""", + f"""a {r},{r} 0 0,0 {-2*r},{0}""", + ] + + +def _path_commands_for_points_with_two_points( + *, a: complex, b: complex, hint_point: complex, draw_coord: Callable[[complex], complex] +) -> list[str]: + def transform_dif(d: complex) -> complex: + return draw_coord(d) - draw_coord(0) + + da = a - hint_point + db = b - hint_point + angle = math.atan2(da.imag, da.real) - math.atan2(db.imag, db.real) + angle %= math.pi * 2 + if angle < math.pi: + a, b = b, a + + if abs(abs(da) - abs(db)) < 1e-4 < abs(da + db): + # Semi-circle oriented towards measure qubit. + draw_a = draw_coord(a) + draw_ba = transform_dif(b - a) + aspect_x = 1.0 + aspect_z = 1.0 + + # Try to squash the oval slightly, so when two face each other there's a small gap. + if b.imag == a.imag: + aspect_z = 0.8 + elif b.real == a.real: + aspect_x = 0.8 + + return [ + f"""M {draw_a.real},{draw_a.imag}""", + f"""a {aspect_x},{aspect_z} 0 0,0 {draw_ba.real},{draw_ba.imag}""", + f"""L {draw_a.real},{draw_a.imag}""", + ] + else: + # A wedge between the two data qubits. + dif = b - a + average = (a + b) * 0.5 + perp = dif * 1j + if abs(perp) > 1: + perp /= abs(perp) + ac1 = average + perp * 0.2 - dif * 0.2 + ac2 = average + perp * 0.2 + dif * 0.2 + bc1 = average + perp * -0.2 + dif * 0.2 + bc2 = average + perp * -0.2 - dif * 0.2 + + tac1 = draw_coord(ac1) + tac2 = draw_coord(ac2) + tbc1 = draw_coord(bc1) + tbc2 = draw_coord(bc2) + draw_a = draw_coord(a) + draw_b = draw_coord(b) + return [ + f"M {draw_a.real},{draw_a.imag}", + f"C {tac1.real} {tac1.imag}, {tac2.real} {tac2.imag}, {draw_b.real} {draw_b.imag}", + f"C {tbc1.real} {tbc1.imag}, {tbc2.real} {tbc2.imag}, {draw_a.real} {draw_a.imag}", + ] + + +def _path_commands_for_points_with_many_points( + *, pts: Sequence[complex], draw_coord: Callable[[complex], complex] +) -> list[str]: + assert len(pts) >= 3 + ori = draw_coord(pts[-1]) + path_commands = [f"""M{ori.real},{ori.imag}"""] + for k in range(len(pts)): + prev_prev_q = pts[k - 2] + prev_q = pts[k - 1] + q = pts[k] + next_q = pts[(k + 1) % len(pts)] + if is_colinear(prev_q, q, next_q) or is_colinear(prev_prev_q, prev_q, q): + prev_pt = draw_coord(prev_q) + cur_pt = draw_coord(q) + d = cur_pt - prev_pt + p1 = prev_pt + d * (-0.25 + 0.05j) + p2 = cur_pt + d * (0.25 + 0.05j) + path_commands.append( + f"""C {p1.real} {p1.imag}, {p2.real} {p2.imag}, {cur_pt.real} {cur_pt.imag}""" + ) + else: + q2 = draw_coord(q) + path_commands.append(f"""L {q2.real},{q2.imag}""") + return path_commands + + +def svg_path_directions_for_tile( + *, + tile: stimflow.Tile, + draw_coord: Callable[[complex], complex], + contract_towards: complex | None = None, +) -> str | None: + hint_point = tile.measure_qubit + if hint_point is None or any(abs(q - hint_point) < 1e-4 for q in tile.data_set): + hint_point = sum(tile.data_set) / (len(tile.data_set) or 1) + + points = sorted( + tile.data_set, + key=lambda p2: math.atan2(p2.imag - hint_point.imag, p2.real - hint_point.real), + ) + + if len(points) == 0: + return None + + if len(points) == 1: + return " ".join( + _path_commands_for_points_with_one_point(a=points[0], draw_coord=draw_coord) + ) + + if len(points) == 2: + return " ".join( + _path_commands_for_points_with_two_points( + a=points[0], b=points[1], hint_point=hint_point, draw_coord=draw_coord + ) + ) + + if contract_towards is not None: + c = 0.8 + points = [p * c + (1 - c) * contract_towards for p in points] + + return " ".join(_path_commands_for_points_with_many_points(pts=points, draw_coord=draw_coord)) + + +def _draw_obs( + *, + obj: stimflow.StabilizerCode, + observable_style: str, + labels_out: list[tuple[complex, str, dict[str, Any]]], + scale_factor: float, + out_lines: list[str], + q2p: Callable[[complex], complex], +): + from stimflow._core import PauliMap + + if observable_style == "label": + combined = obj.logicals + obj.scattered_logicals + for k in range(len(combined)): + if len(combined) <= 1: + suffix = "" + elif k < 10: + suffix = "₀₁₂₃₄₅₆₇₈₉"[k] + else: + suffix = str(k) + entry = combined[k] + cases: list[PauliMap] + if isinstance(entry, PauliMap): + prefixes = ["L"] + cases = [entry] + else: + obs_a, obs_b = entry + cases = [obs_a, obs_b] + prefixes = [ + (next(iter(set(obs_a.values()))) if len(set(obs_a.values())) == 1 else "A"), + (next(iter(set(obs_b.values()))) if len(set(obs_b.values())) == 1 else "B"), + ] + for prefix, obs in zip(prefixes, cases): + for q, basis2 in obs.items(): + label = prefix + suffix + if prefix != "L" and basis2 != prefix: + label += "[" + basis2 + "]" + labels_out.append( + ( + q, + label, + { + "text-anchor": "end", + "dominant-baseline": "hanging", + "font-size": scale_factor * 0.6, + "fill": BASE_COLORS_DARK[basis2], + }, + ) + ) + elif observable_style == "circles": + for obs in obj.flat_logicals: + for q, p in sorted(obs.items(), key=lambda e: (e[0].real, e[0].imag)): + c = q2p(q) + out_lines.append( + f"""""" + ) + elif observable_style == "polygon": + for obs in obj.flat_logicals: + path_directions = svg_path_directions_for_tile(tile=obs.to_tile(), draw_coord=q2p) + fill_color = BASE_COLORS[obs.to_tile().basis] + out_lines.append( + f'''""" + ) + else: + raise NotImplementedError(f"{observable_style=}") + + +def _draw_patch( + *, + obj: stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface | stim.Circuit, + q2p: Callable[[complex], complex], + show_coords: bool, + show_obs: bool, + opacity: float, + show_data_qubits: bool, + show_measure_qubits: bool, + system_qubits: frozenset[complex], + clip_path_id_ptr: list[int], + out_lines: list[str], + show_order: bool, + find_logical_err_max_weight: int | None, + tile_color_func: ( + Callable[ + [stimflow.Tile], str | tuple[float, float, float] | tuple[float, float, float, float] | None + ] + | None + ), + stabilizer_style: Literal["polygon", "circles"] | None, + observable_style: Literal["label", "polygon", "circles"], +) -> None: + if isinstance(obj, stim.Circuit): + from stimflow._viz._viz_circuit_layer_svg import append_circuit_layer_to_svg + + append_circuit_layer_to_svg(circuit=obj, lines=out_lines, q2p=q2p) + return + + layer_1q2: list[str] = [] + layer_1q: list[str] = [] + fill_layer2q: list[str] = [] + fill_layer_mq: list[str] = [] + stroke_layer_mq: list[str] = [] + scale_factor = abs(q2p(1) - q2p(0)) + + from stimflow._chunk import ChunkInterface, StabilizerCode + from stimflow._core import Tile + + if isinstance(obj, ChunkInterface): + obj = obj.to_code() + show_order = False + + labels: list[tuple[complex, str, dict[str, Any]]] = [] + if isinstance(obj, StabilizerCode): + if find_logical_err_max_weight is not None: + try: + err = obj.find_logical_error(max_search_weight=find_logical_err_max_weight) + except ValueError as ex: + print( + f"WARNING: No logical error will be drawn.\n Reason: {ex}", + file=sys.stderr, + ) + err = [] + for e in err: + for loc in e.circuit_error_locations: + for loc2 in loc.flipped_pauli_product: + real, imag = loc2.coords + q = real + 1j * imag + p = loc2.gate_target.pauli_type + labels.append( + ( + q, + p + "!", + { + "text-anchor": "middle", + "dominant-baseline": "central", + "font-size": scale_factor * 1.1, + "fill": BASE_COLORS_DARK[p], + }, + ) + ) + + if isinstance(obj, StabilizerCode) and show_obs: + _draw_obs( + out_lines=stroke_layer_mq, + obj=obj, + observable_style=observable_style, + labels_out=labels, + q2p=q2p, + scale_factor=scale_factor, + ) + + for q, s, ts in labels: + loc2 = q2p(q) + terms = {"x": loc2.real, "y": loc2.imag, **ts} + layer_1q2.append( + "{s}" + ) + + all_points = set(system_qubits) + if show_data_qubits: + all_points |= obj.data_set + if show_measure_qubits: + all_points |= obj.measure_set + if show_coords and all_points: + all_x = sorted({q.real for q in all_points}) + all_y = sorted({q.imag for q in all_points}) + left = min(all_x) - 1 + top = min(all_y) - 1 + + for x in all_x: + if x == int(x): + x = int(x) + loc2 = q2p(x + top * 1j) + stroke_layer_mq.append( + "{x}" + ) + for y in all_y: + if y == int(y): + y = int(y) + loc2 = q2p(y * 1j + left) + stroke_layer_mq.append( + "{y}i" + ) + + sorted_tiles = sorted(obj.tiles, key=tile_data_span, reverse=True) + d2tiles: collections.defaultdict[complex, list[Tile]] = collections.defaultdict(list) + + def contraction_point(tile) -> complex | None: + if len(tile.data_set) <= 2: + return None + + # Inset tiles that overlap with other tiles. + for data_qubit in tile.data_set: + for other_tile in d2tiles[data_qubit]: + if other_tile is not tile: + if tile.data_set < other_tile.data_set or ( + tile.data_set == other_tile.data_set and tile.bases < other_tile.bases + ): + return sum(other_tile.data_set) / len(other_tile.data_set) + + return None + + for tile in sorted_tiles: + for d in tile.data_set: + d2tiles[d].append(tile) + + for tile in sorted_tiles: + c = tile.measure_qubit + if c is None or any(abs(q - c) < 1e-4 for q in tile.data_set): + c = sum(tile.data_set) / max(len(tile.data_set), 1) + dq = sorted(tile.data_set, key=lambda p2: math.atan2(p2.imag - c.imag, p2.real - c.real)) + if not dq: + continue + fill_color: str | tuple[float, float, float] | tuple[float, float, float, float] | None + fill_color = BASE_COLORS[tile.basis] + tile_opacity = opacity + if tile_color_func is not None: + fill_color = tile_color_func(tile) + if fill_color is None: + continue + if isinstance(fill_color, tuple): + r: float + g: float + b: float + if len(fill_color) == 3: + r, g, b = fill_color + else: + a: float + r, g, b, a = fill_color + tile_opacity *= a + fill_color = ( + "#" + + f"{round(r * 255.49):x}".rjust(2, "0") + + f"{round(g * 255.49):x}".rjust(2, "0") + + f"{round(b * 255.49):x}".rjust(2, "0") + ) + if len(tile.data_set) == 1: + fl = layer_1q + sl = stroke_layer_mq + elif len(tile.data_set) == 2: + fl = fill_layer2q + sl = stroke_layer_mq + else: + fl = fill_layer_mq + sl = stroke_layer_mq + cp = contraction_point(tile) + if stabilizer_style == "polygon": + path_directions = svg_path_directions_for_tile( + tile=tile, draw_coord=q2p, contract_towards=cp + ) + elif stabilizer_style == "circles": + for q, p in sorted(tile.to_pauli_map().items(), key=lambda e: (e[0].real, e[0].imag)): + c = q2p(q) + fl.append( + f"""""" + ) + path_directions = None + elif stabilizer_style is None: + path_directions = None + else: + raise NotImplementedError(f"{stabilizer_style=}") + if path_directions is not None: + fl.append( + f'''""" + ) + if cp is None: + sl.append( + f'''""" + ) + + # Add basis glows around data qubits in multi-basis stabilizers. + if path_directions is not None and tile.basis is None and tile_color_func is None: + clip_path_id_ptr[0] += 1 + fl.append(f'') + fl.append(f""" """) + fl.append("") + for k, q in enumerate(tile.data_qubits): + if q is None: + continue + v = q2p(q) + fl.append( + f"' + ) + + drawn_qubits: set[complex] = set() + if show_data_qubits: + drawn_qubits |= obj.data_set + for q in obj.data_set: + loc2 = q2p(q) + layer_1q2.append( + f"""" + ) + + if show_measure_qubits: + drawn_qubits |= obj.measure_set + for q in obj.measure_set: + loc2 = q2p(q) + layer_1q2.append( + f"""" + ) + + for q in system_qubits: + if q not in drawn_qubits: + loc2 = q2p(q) + layer_1q2.append( + f"""" + ) + + out_lines += fill_layer_mq + out_lines += stroke_layer_mq + out_lines += fill_layer2q + out_lines += layer_1q + out_lines += layer_1q2 + + # Draw each element's measurement order as a zig zag arrow. + if show_order: + for tile in obj.tiles: + _draw_tile_order_arrow(q2p=q2p, tile=tile, out_lines=out_lines) + + +BASE_COLORS = {"X": "#FF8080", "Z": "#8080FF", "Y": "#80FF80", None: "#CCC"} +BASE_COLORS_DARK = {"X": "#B01010", "Z": "#1010B0", "Y": "#10B010", None: "black"} + + +def tile_data_span(tile: stimflow.Tile) -> Any: + from stimflow._core import min_max_complex + + min_c, max_c = min_max_complex(tile.data_set, default=0) + return max_c.real - min_c.real + max_c.imag - min_c.imag, tile.bases + + +def _draw_tile_order_arrow( + *, tile: stimflow.Tile, q2p: Callable[[complex], complex], out_lines: list[str] +): + scale_factor = abs(q2p(1) - q2p(0)) + + c = tile.measure_qubit + if c is None: + c = sum(tile.data_set) / (len(tile.data_set) or 1) + if len(tile.data_set) == 3 or c in tile.data_set: + c = 0 + for q in tile.data_set: + c += q + c /= len(tile.data_set) + pts: list[complex] = [] + + path_cmd_start = '' + ) + delay = 0 + prev = v + else: + delay += 1 + path_cmd_start = path_cmd_start.strip() + path_cmd_start += ( + f'" fill="none" stroke-width="{scale_factor * 0.02}" stroke="{arrow_color}" />' + ) + out_lines.append(path_cmd_start) + + # Draw arrow at end of arrow. + if len(pts) > 1: + p = pts[-1] + d2 = p - pts[-2] + if d2: + d2 /= abs(d2) + d2 *= 4 * scale_factor * 0.02 + a = p + d2 + b = p + d2 * 1j + c = p + d2 * -1j + out_lines.append( + f"' + ) diff --git a/glue/stimflow/src/stimflow/_viz/_viz_patch_svg_test.py b/glue/stimflow/src/stimflow/_viz/_viz_patch_svg_test.py new file mode 100644 index 00000000..82952f10 --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_viz_patch_svg_test.py @@ -0,0 +1,34 @@ +import stimflow + + +def test_patch_svg_runs(): + patch = stimflow.Patch( + tiles=[ + stimflow.Tile(data_qubits=(None, 1j, None, 2j), measure_qubit=(-0.5 + 1.5j), bases="Z"), + stimflow.Tile(data_qubits=(None, 0j, None, (1 + 0j)), measure_qubit=(0.5 - 0.5j), bases="X"), + stimflow.Tile( + data_qubits=(0j, (1 + 0j), 1j, (1 + 1j)), measure_qubit=(0.5 + 0.5j), bases="Z" + ), + stimflow.Tile( + data_qubits=(1j, 2j, (1 + 1j), (1 + 2j)), measure_qubit=(0.5 + 1.5j), bases="X" + ), + stimflow.Tile( + data_qubits=((1 + 0j), (1 + 1j), (2 + 0j), (2 + 1j)), + measure_qubit=(1.5 + 0.5j), + bases="X", + ), + stimflow.Tile( + data_qubits=((1 + 1j), (2 + 1j), (1 + 2j), (2 + 2j)), + measure_qubit=(1.5 + 1.5j), + bases="Z", + ), + stimflow.Tile( + data_qubits=((1 + 2j), None, (2 + 2j), None), measure_qubit=(1.5 + 2.5j), bases="X" + ), + stimflow.Tile( + data_qubits=((2 + 0j), None, (2 + 1j), None), measure_qubit=(2.5 + 0.5j), bases="Z" + ), + ] + ) + svg_content = stimflow.svg([patch]) + assert svg_content is not None diff --git a/glue/stimflow/src/stimflow/_viz/_viz_svg.py b/glue/stimflow/src/stimflow/_viz/_viz_svg.py new file mode 100644 index 00000000..e120347b --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_viz_svg.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import math +from collections.abc import Callable, Iterable +from typing import Literal, TYPE_CHECKING + +import stim + +from stimflow._viz._viz_patch_svg import _draw_patch + +if TYPE_CHECKING: + import stimflow + + +def svg( + objects: Iterable[stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface | stim.Circuit], + *, + background: stimflow.Patch | stimflow.StabilizerCode | stimflow.ChunkInterface | stim.Circuit | None = None, + title: str | list[str] | None = None, + canvas_height: int | None = None, + show_order: bool = False, + show_obs: bool = True, + opacity: float = 1, + show_measure_qubits: bool = True, + show_data_qubits: bool = False, + system_qubits: Iterable[complex] = (), + show_all_qubits: bool = False, + extra_used_coords: Iterable[complex] = (), + show_coords: bool = True, + find_logical_err_max_weight: int | None = None, + rows: int | None = None, + cols: int | None = None, + tile_color_func: ( + Callable[ + [stimflow.Tile], str | tuple[float, float, float] | tuple[float, float, float, float] | None + ] + | None + ) = None, + stabilizer_style: Literal["polygon", "circles"] | None = "polygon", + observable_style: Literal["label", "polygon", "circles"] = "label", + show_frames: bool = True, + pad: float | None = None, +) -> stimflow.str_svg: + """Returns an SVG image of the given objects.""" + system_qubits = frozenset(system_qubits) + if canvas_height is None: + canvas_height = 500 + + extra_used_coords = frozenset(extra_used_coords) + from stimflow._layers import LayerCircuit + + patches = tuple( + patch.to_stim_circuit() if isinstance(patch, LayerCircuit) else patch for patch in objects + ) + all_points: set[complex] = set() + all_points.update(system_qubits) + all_points.update(extra_used_coords) + for patch in patches: + if isinstance(patch, stim.Circuit): + all_points.update( + v[0] + v[1] * 1j for v in patch.get_final_qubit_coordinates().values() + ) + else: + all_points.update(patch.used_set) + if show_all_qubits: + system_qubits = frozenset(all_points) + from stimflow._core import min_max_complex + + min_c, max_c = min_max_complex(all_points, default=0) + min_c -= 0.5 + 0.5j + max_c += 0.5 + 0.5j + offset: complex = 0 + if title is not None: + min_c -= 1j + offset += 1j + if show_coords: + min_c -= 1 + 1j + box_width = max_c.real - min_c.real + box_height = max_c.imag - min_c.imag + if pad is None: + pad = max(box_width, box_height) * 0.01 + 0.25 + box_x_pitch = box_width + pad + box_y_pitch = box_height + pad + if cols is None and rows is None: + cols = min(len(patches), math.ceil(math.sqrt(len(patches) * 2))) + rows = math.ceil(len(patches) / max(1, cols)) + elif cols is None: + cols = math.ceil(len(patches) / max(1, rows)) + elif rows is None: + rows = math.ceil(len(patches) / max(1, cols)) + else: + assert cols * rows >= len(patches) + total_height = max(1.0, box_y_pitch * rows - pad + offset.imag) + total_width = max(1.0, box_x_pitch * cols - pad + offset.real) + scale_factor = canvas_height / max(total_height, 1) + canvas_width = int(math.ceil(canvas_height * (total_width / total_height))) + + def patch_q2p(patch_index: int, q: complex) -> complex: + q -= min_c + q += offset + q += box_x_pitch * (patch_index % cols) + q += box_y_pitch * (patch_index // cols) * 1j + q *= scale_factor + return q + + lines = [ + f"""""" + ] + + if isinstance(title, str): + lines.append( + f"{title}" + ) + elif title is not None: + for plan_i, part in enumerate(title): + lines.append( + f"{part}" + ) + + clip_path_id_ptr = [0] + for plan_i, plan in enumerate(patches): + layers = [plan] + if background is not None: + if isinstance(background, (tuple, list)): + layers.insert(0, background[plan_i % len(background)]) + else: + layers.insert(0, background) + for layer in layers: + _draw_patch( + obj=layer, + q2p=lambda q: patch_q2p(plan_i, q), + show_coords=show_coords, + opacity=opacity, + show_data_qubits=show_data_qubits, + show_measure_qubits=show_measure_qubits, + system_qubits=system_qubits, + clip_path_id_ptr=clip_path_id_ptr, + out_lines=lines, + show_order=show_order, + show_obs=show_obs, + find_logical_err_max_weight=find_logical_err_max_weight, + tile_color_func=tile_color_func, + stabilizer_style=stabilizer_style, + observable_style=observable_style, + ) + + # Draw frame outlines + if show_frames: + for outline_index in range(len(patches)): + a = patch_q2p(outline_index, min_c) + a += offset + b = patch_q2p(outline_index, max_c) + lines.append( + f'' + ) + + lines.append("") + from stimflow._core import str_svg + + return str_svg("\n".join(lines)) diff --git a/glue/stimflow/tools/gen_api_reference.py b/glue/stimflow/tools/gen_api_reference.py new file mode 100755 index 00000000..1de19fbf --- /dev/null +++ b/glue/stimflow/tools/gen_api_reference.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 + +import dataclasses +import inspect +import types +from collections.abc import Iterator +from typing import Any, cast + +import stimflow + +keep = { + "__add__", + "__bool__", + "__contains__", + "__mod__", + "__rmod__", + "__radd__", + "__eq__", + "__call__", + "__ge__", + "__getitem__", + "__gt__", + "__iadd__", + "__imul__", + "__init__", + "__truediv__", + "__itruediv__", + "__ne__", + "__neg__", + "__le__", + "__len__", + "__lt__", + "__mul__", + "__setitem__", + "__str__", + "__pos__", + "__pow__", + "__repr__", + "__rmul__", + "__hash__", + "__iter__", + "__next__", +} +skip = { + "__firstlineno__", + "__static_attributes__", + "__getnewargs__", + "__replace__", + "__builtins__", + "__cached__", + "__getstate__", + "__setstate__", + "__path__", + "__class__", + "__delattr__", + "__dir__", + "__doc__", + "__file__", + "__format__", + "__getattribute__", + "__init_subclass__", + "__loader__", + "__module__", + "__name__", + "__new__", + "__package__", + "__reduce__", + "__reduce_ex__", + "__setattr__", + "__sizeof__", + "__spec__", + "__subclasshook__", + "__version__", + "__annotations__", + "__dataclass_fields__", + "__dataclass_params__", + "__dict__", + "__match_args__", + "__post_init__", + "__weakref__", + "__abstractmethods__", +} + + +def normalize_doc_string(d: str) -> str: + lines = d.splitlines() + indented_lines = [e for e in lines[1:] if e.strip()] + indentation = min([len(line) - len(line.lstrip()) for line in indented_lines], default=0) + return "\n".join(lines[:1] + [e[indentation:] for e in lines[1:]]) + + +def indented(*, paragraph: str, indentation: str) -> str: + return "".join( + indentation * (line != "\n") + line for line in paragraph.splitlines(keepends=True) + ) + + +class DescribedObject: + def __init__(self): + self.full_name = "" + self.level = 0 + self.lines = [] + + +def splay_signature(sig: str) -> list[str]: + # Maintain backwards compatibility with python 3.6 + sig = sig.replace("pathlib._local.Path", "pathlib.Path") + + assert sig.startswith("def") + out = [] + + level = 0 + + start = sig.index("(") + 1 + mark = start + out.append(sig[:mark]) + for k in range(mark, len(sig)): + c = sig[k] + if c in "([": + level += 1 + if c in "])": + level -= 1 + if (c == "," and level == 0) or level < 0: + k2 = k + (0 if level < 0 else 1) + s = sig[mark:k2].lstrip() + if s: + if not s.endswith(","): + s += "," + s = s.replace("':", ":") + s = s.replace(" -> '", " -> ") + s = s.replace(": '", ": ") + s = s.replace("',", ",") + s = s.replace("' = ", " = ") + out.append(" " + s) + mark = k2 + if level < 0: + break + assert level == -1 + s = sig[mark:] + s = s.replace("':", ":") + s = s.replace(" -> '", " -> ") + out.append(s) + return out + + +def _handle_pybind_method( + *, obj: Any, is_property: bool, out_obj: DescribedObject, parent: Any, full_name: str +) -> tuple[str, bool, str, str]: + doc = normalize_doc_string(getattr(obj, "__doc__", "") or "") + if is_property: + out_obj.lines.append("@property") + doc_lines = doc.splitlines() + new_args_name = None + was_args = False + sig_handled = False + has_setter = False + doc_lines_left = [] + term_name = full_name.split(".")[-1] + for line in doc_lines: + if was_args and line.strip().startswith("*") and ":" in line: + new_args_name = line[line.index("*") : line.index(":")] + doc_lines_left.append(line) + was_args = "Args:" in line + + if is_property: + sig_name = f"{term_name}(self)" + if getattr(obj, "fset", None) is not None: + has_setter = True + elif doc_lines_left[0].startswith(term_name): + sig_name = term_name + doc_lines_left[0][len(term_name) :] + doc_lines_left = doc_lines_left[1:] + else: + sig_name = term_name + + doc = "\n".join(doc_lines_left).lstrip() + text = "" + if not sig_handled: + if "(self: " in sig_name: + k_low = sig_name.index("(self: ") + len("(self") + k_high = len(sig_name) + if "->" in sig_name: + k_high = sig_name.index("->", k_low, k_high) + k_high = sig_name.index(", " if ", " in sig_name[k_low:k_high] else ")", k_low, k_high) + sig_name = sig_name[:k_low] + sig_name[k_high:] + if not sig_handled: + is_static = "(self" not in sig_name and inspect.isclass(parent) + if is_static: + out_obj.lines.append("@staticmethod") + sig_name = sig_name.replace(": handle", ": Any") + sig_name = sig_name.replace("numpy.", "np.") + if new_args_name is not None: + sig_name = sig_name.replace("*args", new_args_name) + text = "\n".join(splay_signature(f"def {sig_name}:")) + return text, has_setter, doc, sig_name + + +def print_doc(*, full_name: str, parent: object, obj: object, level: int) -> DescribedObject | None: + out_obj = DescribedObject() + out_obj.full_name = full_name + out_obj.level = level + doc = getattr(obj, "__doc__", "") or "" + doc = normalize_doc_string(doc) + if full_name.endswith("__") and len(doc.splitlines()) <= 2: + return None + + term_name = full_name.split(".")[-1] + is_property = isinstance(obj, property) + is_method = doc.startswith(term_name) + has_setter = False + is_normal_method = isinstance(obj, types.FunctionType) + sig_name = "" + if "stimflow" in full_name and is_normal_method: + text = "" + if term_name in getattr(parent, "__abstractmethods__", []): + text += "@abc.abstractmethod\n" + sig_name = f"{term_name}{inspect.signature(cast(Any, obj))}" + text += "\n".join(splay_signature(f"def {sig_name}:")) + + # Replace default value lambdas with their source. + if "lambda" in str(text): + for param in inspect.signature(cast(Any, obj)).parameters.values(): + if "lambda" in str(param.default): + _, lambda_src = inspect.getsource(param.default).split("lambda ") + lambda_src = lambda_src.strip() + lambda_src = "lambda " + lambda_src + text = text.replace(str(param.default), lambda_src) + text = text.replace(",,", ",") + + text = text.replace("numpy.", "np.") + elif is_method or is_property: + text, has_setter, doc, sig_name = _handle_pybind_method( + obj=obj, is_property=is_property, out_obj=out_obj, parent=parent, full_name=full_name + ) + elif isinstance(obj, (int, str)): + text = f"{term_name}: {type(obj).__name__} = {obj!r}" + doc = "" + elif term_name == term_name.upper(): + return None # Skip constants because they lack a doc string. + else: + text = f"class {term_name}" + if inspect.isabstract(obj): + text += "(metaclass=abc.ABCMeta)" + text += ":" + if doc: + if text: + text += "\n" + text += indented(paragraph=f'"""{doc.rstrip()}\n"""', indentation=" ") + + dataclass_fields = getattr(obj, "__dataclass_fields__", []) + if dataclass_fields: + dataclass_prop = "@dataclasses.dataclass" + if getattr(obj, "__dataclass_params__").frozen: + dataclass_prop += "(frozen=True)" + out_obj.lines.append(dataclass_prop) + + out_obj.lines.append(text.replace("._stim_avx2", "").replace("._stim_sse2", "")) + if has_setter: + if "->" in sig_name: + setter_type = sig_name[sig_name.index("->") + 2 :].strip().replace("._stim_avx2", "") + else: + setter_type = "Any" + out_obj.lines.append(f"@{term_name}.setter") + out_obj.lines.append(f"def {term_name}(self, value: {setter_type}):") + out_obj.lines.append(" pass") + + if dataclass_fields: + for f in dataclasses.fields(cast(Any, obj)): + if str(f.type).startswith("typing"): + t = str(f.type).replace("typing.", "") + else: + t = str(f.type) + t = t.replace( + "Union[Dict[str, ForwardRef('JSON_TYPE')], " + "List[ForwardRef('JSON_TYPE')], str, int, float]", + "Any", + ) + if f.default is dataclasses.MISSING: + out_obj.lines.append(f" {f.name}: {t}") + else: + out_obj.lines.append(f" {f.name}: {t} = {f.default}") + + return out_obj + + +def generate_documentation(*, obj: object, level: int, full_name: str) -> Iterator[DescribedObject]: + if full_name.endswith("__"): + return + if not inspect.ismodule(obj) and not inspect.isclass(obj): + return + + for sub_name in dir(obj): + if sub_name in getattr(obj, "__dataclass_fields__", []): + continue + if sub_name in skip: + continue + if sub_name.startswith("__pybind11"): + continue + if sub_name.startswith("_") and not sub_name.startswith("__"): + continue + if sub_name.endswith("__") and sub_name not in keep: + raise ValueError("Need to classify " + sub_name + " as keep or skip.") + sub_full_name = full_name + "." + sub_name + sub_obj = getattr(obj, sub_name) + if full_name.endswith("str_svg"): + pass + if isinstance(obj, type) and sub_name not in obj.__dict__: + continue + v = print_doc(full_name=sub_full_name, obj=sub_obj, level=level + 1, parent=obj) + if v is not None: + yield v + yield from generate_documentation(obj=sub_obj, level=level + 1, full_name=sub_full_name) + + +def main(): + objects = [ + obj + for obj in generate_documentation(obj=stimflow, full_name="stimflow", level=0) + if all("[DEPRECATED]" not in line for line in obj.lines) + ] + + print(f"# stimflow v{stimflow.__version__} API Reference") + print() + print("## Index") + for obj in objects: + level = obj.level + print((level - 1) * " " + f"- [`{obj.full_name}`](#{obj.full_name})") + + print( + """ +```python +# Types used by the method definitions. +from __future__ import annotations +from typing import overload, TYPE_CHECKING, Any, Iterable +import io +import pathlib +import numpy as np +``` +""".strip() + ) + + for obj in objects: + print() + print(f'') + print("```python") + print(f"# {obj.full_name}") + print() + if len(obj.full_name.split(".")) > 2: + print(f'# (in class {".".join(obj.full_name.split(".")[:-1])})') + else: + print("# (at top-level in the stimflow module)") + print("\n".join(obj.lines)) + print("```") + + +if __name__ == "__main__": + main() From c1ca805b8068d4cf196adbc916b34641bbcd4cf6 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 14:03:50 -0700 Subject: [PATCH 2/9] Some documentation and typo fixes and renaming --- .github/workflows/ci.yml | 2 +- dev/doctest_proper.py | 9 +- glue/stimflow/doc/api.md | 291 +++++++++++++----- glue/stimflow/setup.py | 6 +- glue/stimflow/src/stimflow/__init__.py | 7 +- glue/stimflow/src/stimflow/_core/__init__.py | 1 - .../src/stimflow/_core/_complex_util.py | 99 ++++-- glue/stimflow/src/stimflow/_viz/_3d_model.py | 194 +++++++++--- .../src/stimflow/_viz/_3d_model_test.py | 16 +- .../src/stimflow/_viz/_3d_model_viewer.py | 3 +- glue/stimflow/src/stimflow/_viz/__init__.py | 6 +- 11 files changed, 472 insertions(+), 162 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46031c49..e1557c77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -480,7 +480,7 @@ jobs: - run: pip install -e glue/stimflow - run: pip install pytest - run: pytest glue/stimflow - - run: dev/doctest_proper.py --module stimflow + - run: dev/doctest_proper.py --module stimflow --suppress_examples_warning_for stimflow.str_svg stimflow.str_html test_sinter: runs-on: ubuntu-24.04 steps: diff --git a/dev/doctest_proper.py b/dev/doctest_proper.py index 4557ec0a..b5a49b28 100755 --- a/dev/doctest_proper.py +++ b/dev/doctest_proper.py @@ -79,6 +79,12 @@ def main(): nargs='*', type=str, help="Modules to import for each doctest.") + parser.add_argument( + '--suppress_examples_warning_for', + default=(), + nargs='*', + type=str, + help="Objects that don't need an 'examples:' section in their documentation.") args = parser.parse_args() globs = { @@ -96,7 +102,8 @@ def main(): if '\n' in v.strip() and 'examples:' not in v and 'example:' not in v and '[deprecated]' not in v: if k.split('.')[-1] not in ['__format__', '__next__', '__iter__', '__init_subclass__', '__module__', '__eq__', '__ne__', '__str__', '__repr__']: if all(not (e.startswith('_') and not e.startswith('__')) for e in k.split('.')): - print(f" Warning: Missing 'examples:' section in docstring of {k!r}", file=sys.stderr) + if all(not k.startswith(prefix) for prefix in args.suppress_examples_warning_for): + print(f" Warning: Missing 'examples:' section in docstring of {k!r}", file=sys.stderr) module.__test__ = {k: v for k, v in out.items()} if doctest.testmod(module, globs=globs).failed: diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 2f64dcd4..1ff1c0dc 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -128,9 +128,9 @@ - [`stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type`](#stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type) - [`stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier`](#stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier) - [`stimflow.LayerCircuit.without_empty_layers`](#stimflow.LayerCircuit.without_empty_layers) -- [`stimflow.LineData`](#stimflow.LineData) - - [`stimflow.LineData.__init__`](#stimflow.LineData.__init__) - - [`stimflow.LineData.fused`](#stimflow.LineData.fused) +- [`stimflow.LineDataFor3DModel`](#stimflow.LineDataFor3DModel) + - [`stimflow.LineDataFor3DModel.__init__`](#stimflow.LineDataFor3DModel.__init__) + - [`stimflow.LineDataFor3DModel.fused`](#stimflow.LineDataFor3DModel.fused) - [`stimflow.MeasureLayer`](#stimflow.MeasureLayer) - [`stimflow.MeasureLayer.append_into_stim_circuit`](#stimflow.MeasureLayer.append_into_stim_circuit) - [`stimflow.MeasureLayer.copy`](#stimflow.MeasureLayer.copy) @@ -230,8 +230,8 @@ - [`stimflow.StimCircuitLoom.weave`](#stimflow.StimCircuitLoom.weave) - [`stimflow.StimCircuitLoom.weaved_target_rec_from_c0`](#stimflow.StimCircuitLoom.weaved_target_rec_from_c0) - [`stimflow.StimCircuitLoom.weaved_target_rec_from_c1`](#stimflow.StimCircuitLoom.weaved_target_rec_from_c1) -- [`stimflow.TextData`](#stimflow.TextData) - - [`stimflow.TextData.__init__`](#stimflow.TextData.__init__) +- [`stimflow.TextDataFor3DModel`](#stimflow.TextDataFor3DModel) + - [`stimflow.TextDataFor3DModel.__init__`](#stimflow.TextDataFor3DModel.__init__) - [`stimflow.Tile`](#stimflow.Tile) - [`stimflow.Tile.__init__`](#stimflow.Tile.__init__) - [`stimflow.Tile.basis`](#stimflow.Tile.basis) @@ -246,10 +246,10 @@ - [`stimflow.Tile.with_transformed_bases`](#stimflow.Tile.with_transformed_bases) - [`stimflow.Tile.with_transformed_coords`](#stimflow.Tile.with_transformed_coords) - [`stimflow.Tile.with_xz_flipped`](#stimflow.Tile.with_xz_flipped) -- [`stimflow.TriangleData`](#stimflow.TriangleData) - - [`stimflow.TriangleData.__init__`](#stimflow.TriangleData.__init__) - - [`stimflow.TriangleData.fused`](#stimflow.TriangleData.fused) - - [`stimflow.TriangleData.rect`](#stimflow.TriangleData.rect) +- [`stimflow.TriangleDataFor3DModel`](#stimflow.TriangleDataFor3DModel) + - [`stimflow.TriangleDataFor3DModel.__init__`](#stimflow.TriangleDataFor3DModel.__init__) + - [`stimflow.TriangleDataFor3DModel.fused`](#stimflow.TriangleDataFor3DModel.fused) + - [`stimflow.TriangleDataFor3DModel.rect`](#stimflow.TriangleDataFor3DModel.rect) - [`stimflow.Viewable3dModelGLTF`](#stimflow.Viewable3dModelGLTF) - [`stimflow.Viewable3dModelGLTF.html_viewer`](#stimflow.Viewable3dModelGLTF.html_viewer) - [`stimflow.append_reindexed_content_to_circuit`](#stimflow.append_reindexed_content_to_circuit) @@ -257,7 +257,6 @@ - [`stimflow.circuit_to_dem_target_measurement_records_map`](#stimflow.circuit_to_dem_target_measurement_records_map) - [`stimflow.circuit_with_xz_flipped`](#stimflow.circuit_with_xz_flipped) - [`stimflow.compile_chunks_into_circuit`](#stimflow.compile_chunks_into_circuit) -- [`stimflow.complex_key`](#stimflow.complex_key) - [`stimflow.count_measurement_layers`](#stimflow.count_measurement_layers) - [`stimflow.find_d1_error`](#stimflow.find_d1_error) - [`stimflow.find_d2_error`](#stimflow.find_d2_error) @@ -2186,24 +2185,41 @@ def without_empty_layers( """ ``` - + ```python -# stimflow.LineData +# stimflow.LineDataFor3DModel # (at top-level in the stimflow module) -class LineData: +class LineDataFor3DModel: + """Coordinates and colors of lines to draw in a 3d model. + + Example: + >>> import stimflow as sf + >>> red_square_outline = sf.LineDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... edge_list=[ + ... # A square made of four lines. + ... [(0, 0, 0), (0, 1, 0)], + ... [(0, 1, 0), (1, 1, 0)], + ... [(1, 1, 0), (1, 0, 0)], + ... [(1, 0, 0), (0, 0, 0)], + ... ], + ... ) + >>> model = sf.make_3d_model([red_square_outline]) + >>> assert model.html_viewer() is not None + """ ``` - + ```python -# stimflow.LineData.__init__ +# stimflow.LineDataFor3DModel.__init__ -# (in class stimflow.LineData) +# (in class stimflow.LineDataFor3DModel) def __init__( self, *, rgba: tuple[float, float, float, float], - edge_list: np.ndarray, + edge_list: np.ndarray | Iterable[Sequence[Sequence[float]]], ): """Lines with associated color information. @@ -2218,14 +2234,14 @@ def __init__( """ ``` - + ```python -# stimflow.LineData.fused +# stimflow.LineDataFor3DModel.fused -# (in class stimflow.LineData) +# (in class stimflow.LineDataFor3DModel) def fused( - data: Iterable[LineData], -) -> list[LineData]: + data: Iterable[LineDataFor3DModel], +) -> list[LineDataFor3DModel]: """Attempts to combine line data instances into fewer instances. """ ``` @@ -3567,26 +3583,42 @@ def weaved_target_rec_from_c1( """ ``` - + ```python -# stimflow.TextData +# stimflow.TextDataFor3DModel # (at top-level in the stimflow module) -class TextData: +class TextDataFor3DModel: + """Details about text to draw in a 3d model. + + The intent is to draw the text as a filled rectangle containing the text. + The data specifies the orientation of the rectangle and the text to place inside of it. + + Example: + >>> import stimflow as sf + >>> hello_banner = sf.TextDataFor3DModel( + ... text='hello', + ... start=(0, 0, 0), + ... forward=(1, 0, 0), + ... up=(0, 1, 0), + ... ) + >>> model = sf.make_3d_model([hello_banner]) + >>> assert model.html_viewer() is not None + """ ``` - + ```python -# stimflow.TextData.__init__ +# stimflow.TextDataFor3DModel.__init__ -# (in class stimflow.TextData) +# (in class stimflow.TextDataFor3DModel) def __init__( self, *, text: str, - start: Sequence[float], - forward: Sequence[float], - up: Sequence[float], + start: tuple[float, float, float] | Sequence[float], + forward: tuple[float, float, float] | Sequence[float], + up: tuple[float, float, float] | Sequence[float], mirror_backside: bool = True, ): """Describes a rectangle showing text. @@ -3597,12 +3629,14 @@ def __init__( This is the `bottom_left` of the rectangle, in 3d. forward: The 3d direction along which the text grows as the message gets longer. This is the `bottom_right - bottom_left` of the rectangle, in 3d. - The length of this vector is ignored. - up: A 3d direction along which the text is oriented. + The length of this vector is ignored; the length of the rectangle is determined + automatically from the desired text. + up: The 3d direction along which the text is oriented. This is the `top_left - bottom_left` of the rectangle, in 3d. - The length of this vector is ignored. + The length of this vector is ignored; the height of the rectangle is determined + automatically from the text. Should be perpendicular to `forward`. - mirror_backside: Determines whether or not the text on the back of the rectangle + mirror_backside: Determines whether the text on the back of the rectangle is mirrored (making it readable) or not (keeping the forward direction consistent). Defaults to True (readable on both sides). """ @@ -3789,24 +3823,39 @@ def with_xz_flipped( ) -> Tile: ``` - + ```python -# stimflow.TriangleData +# stimflow.TriangleDataFor3DModel # (at top-level in the stimflow module) -class TriangleData: +class TriangleDataFor3DModel: + """Coordinates and colors of triangles to draw in a 3d model. + + Example: + >>> import stimflow as sf + >>> red_square = sf.TriangleDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... triangle_list=[ + ... # A square made of two triangles. + ... [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + ... [(1, 1, 0), (0, 1, 0), (1, 0, 0)], + ... ], + ... ) + >>> model = sf.make_3d_model([red_square]) + >>> assert model.html_viewer() is not None + """ ``` - + ```python -# stimflow.TriangleData.__init__ +# stimflow.TriangleDataFor3DModel.__init__ -# (in class stimflow.TriangleData) +# (in class stimflow.TriangleDataFor3DModel) def __init__( self, *, rgba: tuple[float, float, float, float], - triangle_list: np.ndarray, + triangle_list: np.ndarray | Iterable[Sequence[Sequence[float]]], ): """Triangles with associated color information. @@ -3818,33 +3867,46 @@ def __init__( Axis 0 is the triangle axis (each entry is a triangle). Axis 1 is the ABC vertex axis (each entry is a vertex from the triangle). Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). + + Example: + >>> import stimflow as sf + >>> red_square = sf.TriangleDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... triangle_list=[ + ... # A square made of two triangles. + ... [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + ... [(1, 1, 0), (0, 1, 0), (1, 0, 0)], + ... ], + ... ) + >>> model = sf.make_3d_model([red_square]) + >>> assert model.html_viewer() is not None """ ``` - + ```python -# stimflow.TriangleData.fused +# stimflow.TriangleDataFor3DModel.fused -# (in class stimflow.TriangleData) +# (in class stimflow.TriangleDataFor3DModel) def fused( - data: Iterable[TriangleData], -) -> list[TriangleData]: + data: Iterable[TriangleDataFor3DModel], +) -> list[TriangleDataFor3DModel]: """Attempts to combine triangle data instances into fewer instances. """ ``` - + ```python -# stimflow.TriangleData.rect +# stimflow.TriangleDataFor3DModel.rect -# (in class stimflow.TriangleData) +# (in class stimflow.TriangleDataFor3DModel) def rect( *, rgba: tuple[float, float, float, float], origin: Iterable[float], d1: Iterable[float], d2: Iterable[float], -) -> TriangleData: +) -> TriangleDataFor3DModel: """Creates a pair of triangles forming a rectangle. Args: @@ -3986,16 +4048,6 @@ def compile_chunks_into_circuit( """ ``` - -```python -# stimflow.complex_key - -# (at top-level in the stimflow module) -def complex_key( - c: complex, -) -> Any: -``` - ```python # stimflow.count_measurement_layers @@ -4080,13 +4132,13 @@ def html_viewer_for_gltf_model( # (at top-level in the stimflow module) def make_3d_model( - elements: Iterable[TriangleData | LineData | TextData], + elements: Iterable[TriangleDataFor3DModel | LineDataFor3DModel | TextDataFor3DModel], ) -> Viewable3dModelGLTF: - """Creates a 3d model containing the elements. + """Creates a 3d model containing the given elements. Args: elements: A list of objects to include in the model. The list can include triangles - (TriangleData), lines (LineData), and text (TextData). + (TriangleDataFor3DModel), lines (LineDataFor3DModel), and text (TextDataFor3DModel). Returns: The 3d model, as a `stimflow.gltf_model`. @@ -4094,6 +4146,52 @@ def make_3d_model( `stimflow.gltf_model` inherits from `pygltflib.GLTF2` but adds a `_repr_html_` class (creating a 3d viewer in Jupyter notebooks) and a `write_viewer_to` method for saving a standalone HTML viewer. + + Example: + >>> import stimflow as sf + >>> red_square = sf.TriangleDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... triangle_list=[ + ... # A square made of two triangles. + ... [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + ... [(1, 1, 0), (0, 1, 0), (1, 0, 0)], + ... ], + ... ) + >>> blue_square_outline = sf.LineDataFor3DModel( + ... rgba=(0, 0, 1, 1), + ... edge_list=[ + ... # A square made of four lines. + ... [(0, 0, 2), (0, 1, 2)], + ... [(0, 1, 2), (1, 1, 2)], + ... [(1, 1, 2), (1, 0, 2)], + ... [(1, 0, 2), (0, 0, 2)], + ... ], + ... ) + >>> hello_banner = sf.TextDataFor3DModel( + ... text='hello', + ... start=(0, 0, 5), + ... forward=(1, 0, 0), + ... up=(0, 1, 0), + ... ) + >>> model: sf.Viewable3dModelGLTF = sf.make_3d_model([ + ... red_square, + ... hello_banner, + ... blue_square_outline, + ... ]) + >>> viewer: sf.str_html = model.html_viewer() + >>> + >>> # This line is commented out so that running doctest doesn't create a file + >>> # The 'write_to' method writes a file and also announces the written file:// URL to stderr. + >>> # viewer.write_to('tmp.html') + >>> + >>> print(viewer[:162] + "...") + + + + + + + >> import stimflow as sf + >>> sf.min_max_complex([1+2j, 2+1j]) + ((1+1j), (2+2j)) + >>> sf.min_max_complex([1+2j, 2+1j, 1+3j]) + ((1+1j), (2+3j)) + >>> sf.min_max_complex([], default=4+3j) + ((4+3j), (4+3j)) + >>> sf.min_max_complex([1]) + ((1+0j), (1+0j)) + >>> sf.min_max_complex([1, 3, 2]) + ((1+0j), (3+0j)) + >>> sf.min_max_complex([2j, 1j, 3j]) + (1j, 3j) """ ``` @@ -4131,6 +4246,19 @@ def min_max_complex( def sorted_complex( values: Iterable[complex], ) -> list[complex]: + """Sorts complex numbers by real then imaginary coordinate. + + Args: + values: The complex numbers to sort. + + Returns: + The sorted list. + + Examples: + >>> import stimflow as sf + >>> sf.sorted_complex([0, 1, 1j, 1 + 1j]) + [0, 1j, 1, (1+1j)] + """ ``` @@ -4387,5 +4515,26 @@ def xor_sorted( vals: The items to sort. key: An optional key function, mapping the items to keys that determine the sorted order. Unequal items with the same key don't cancel. + + Examples: + >>> import stimflow as sf + >>> sf.xor_sorted([1]) + [1] + >>> sf.xor_sorted([1, 1]) + [] + >>> sf.xor_sorted([1, 1, 1]) + [1] + >>> sf.xor_sorted([1, 1, 1, 1]) + [] + >>> sf.xor_sorted([3, 1, 2, 1]) + [2, 3] + >>> sf.xor_sorted([3, 1, 2, 1, 3]) + [2] + >>> sf.xor_sorted([5, 4, 3, 2, 1, 4]) + [1, 2, 3, 5] + >>> sf.xor_sorted([*range(10), *range(2, 6)]) + [0, 1, 6, 7, 8, 9] + >>> sf.xor_sorted([61, 91, 83, 72, 61], key=lambda e: e % 10) + [91, 72, 83] """ ``` diff --git a/glue/stimflow/setup.py b/glue/stimflow/setup.py index f6368be7..9a9542f1 100644 --- a/glue/stimflow/setup.py +++ b/glue/stimflow/setup.py @@ -16,6 +16,8 @@ with open('README.md', encoding='UTF-8') as f: long_description = f.read() +with open('requirements.txt', encoding='UTF-8') as f: + requirements = f.read().splitlines() __version__ = '1.16.dev0' @@ -32,7 +34,7 @@ long_description=long_description, long_description_content_type='text/markdown', python_requires='>=3.6.0', - data_files=['README.md'], - install_requires=['stim'], + data_files=['README.md', 'requirements.txt'], + install_requires=requirements, tests_require=['pytest', 'python3-distutils'], ) diff --git a/glue/stimflow/src/stimflow/__init__.py b/glue/stimflow/src/stimflow/__init__.py index a5acce57..bb44c84f 100644 --- a/glue/stimflow/src/stimflow/__init__.py +++ b/glue/stimflow/src/stimflow/__init__.py @@ -23,7 +23,6 @@ append_reindexed_content_to_circuit, circuit_to_dem_target_measurement_records_map, circuit_with_xz_flipped, - complex_key, count_measurement_layers, Flow, gate_counts_for_circuit, @@ -49,12 +48,12 @@ transpile_to_z_basis_interaction_circuit, ) from stimflow._viz import ( - LineData, - TriangleData, + LineDataFor3DModel, + TriangleDataFor3DModel, Viewable3dModelGLTF, html_viewer_for_gltf_model, make_3d_model, stim_circuit_html_viewer, svg, - TextData, + TextDataFor3DModel, ) diff --git a/glue/stimflow/src/stimflow/_core/__init__.py b/glue/stimflow/src/stimflow/_core/__init__.py index 389fe56e..94fb45bf 100644 --- a/glue/stimflow/src/stimflow/_core/__init__.py +++ b/glue/stimflow/src/stimflow/_core/__init__.py @@ -9,7 +9,6 @@ stim_circuit_with_transformed_moments, ) from stimflow._core._complex_util import ( - complex_key, min_max_complex, sorted_complex, xor_sorted, diff --git a/glue/stimflow/src/stimflow/_core/_complex_util.py b/glue/stimflow/src/stimflow/_core/_complex_util.py index ef887117..fa869c5b 100644 --- a/glue/stimflow/src/stimflow/_core/_complex_util.py +++ b/glue/stimflow/src/stimflow/_core/_complex_util.py @@ -4,12 +4,21 @@ from typing import Any, cast, TypeVar -def complex_key(c: complex) -> Any: - return c.real != int(c.real), c.real, c.imag +def sorted_complex(values: Iterable[complex]) -> list[complex]: + """Sorts complex numbers by real then imaginary coordinate. + Args: + values: The complex numbers to sort. -def sorted_complex(values: Iterable[complex]) -> list[complex]: - return sorted(values, key=complex_key) + Returns: + The sorted list. + + Examples: + >>> import stimflow as sf + >>> sf.sorted_complex([0, 1, 1j, 1 + 1j]) + [0, 1j, 1, (1+1j)] + """ + return sorted(values, key=lambda e: (e.real, e.imag)) def min_max_complex( @@ -19,15 +28,32 @@ def min_max_complex( Args: coords: The complex numbers to place a bounding box around. - default: If no elements are included, the bounding box will cover this - single value when the collection of complex numbers is empty. If - this argument isn't set (or is set to None), an exception will be - raised instead when given an empty collection. + default: If no elements are included, the returned minimum and maximum + will be equal to this value. If this argument isn't set (or is set to None), + an exception will be raised instead when given an empty collection. The + default value is not used when coords is not empty. Returns: - A pair of complex values (c_min, c_max) where c_min's real component - where c_min is the minimum corner of the bounding box and c_max is the - maximum corner of the bounding box. + A pair of complex values (c_min, c_max) where c_min is the minimum corner of + the bounding box and c_max is the maximum corner of the bounding box. + + Raises: + ValueError: + An empty list of coords was given, and a default value wasn't specified. + Examples: + >>> import stimflow as sf + >>> sf.min_max_complex([1+2j, 2+1j]) + ((1+1j), (2+2j)) + >>> sf.min_max_complex([1+2j, 2+1j, 1+3j]) + ((1+1j), (2+3j)) + >>> sf.min_max_complex([], default=4+3j) + ((4+3j), (4+3j)) + >>> sf.min_max_complex([1]) + ((1+0j), (1+0j)) + >>> sf.min_max_complex([1, 3, 2]) + ((1+0j), (3+0j)) + >>> sf.min_max_complex([2j, 1j, 3j]) + (1j, 3j) """ coords = list(coords) if not coords and default is not None: @@ -54,21 +80,40 @@ def xor_sorted(vals: Iterable[TItem], *, key: Callable[[TItem], Any] | None = No vals: The items to sort. key: An optional key function, mapping the items to keys that determine the sorted order. Unequal items with the same key don't cancel. + + Examples: + >>> import stimflow as sf + >>> sf.xor_sorted([1]) + [1] + >>> sf.xor_sorted([1, 1]) + [] + >>> sf.xor_sorted([1, 1, 1]) + [1] + >>> sf.xor_sorted([1, 1, 1, 1]) + [] + >>> sf.xor_sorted([3, 1, 2, 1]) + [2, 3] + >>> sf.xor_sorted([3, 1, 2, 1, 3]) + [2] + >>> sf.xor_sorted([5, 4, 3, 2, 1, 4]) + [1, 2, 3, 5] + >>> sf.xor_sorted([*range(10), *range(2, 6)]) + [0, 1, 6, 7, 8, 9] + >>> sf.xor_sorted([61, 91, 83, 72, 61], key=lambda e: e % 10) + [91, 72, 83] """ - result = sorted(vals, key=cast(Any, key)) - n = len(result) - skipped = 0 - k = 0 - while k + 1 < n: - if result[k] == result[k + 1]: - skipped += 2 - k += 2 + kept = set() + for v in vals: + if v in kept: + kept.remove(v) else: - result[k - skipped] = result[k] - k += 1 - if k < n: - result[k - skipped] = result[k] - while skipped: - result.pop() - skipped -= 1 - return result + kept.add(v) + + seen = set() + filtered = [] + for v in vals: + if v in kept and v not in seen: + seen.add(v) + filtered.append(v) + + return sorted(filtered, key=cast(Any, key)) diff --git a/glue/stimflow/src/stimflow/_viz/_3d_model.py b/glue/stimflow/src/stimflow/_viz/_3d_model.py index 8863792a..2111a26c 100644 --- a/glue/stimflow/src/stimflow/_viz/_3d_model.py +++ b/glue/stimflow/src/stimflow/_viz/_3d_model.py @@ -11,14 +11,31 @@ from stimflow._viz._3d_model_viewer import Viewable3dModelGLTF -class TextData: +class TextDataFor3DModel: + """Details about text to draw in a 3d model. + + The intent is to draw the text as a filled rectangle containing the text. + The data specifies the orientation of the rectangle and the text to place inside of it. + + Example: + >>> import stimflow as sf + >>> hello_banner = sf.TextDataFor3DModel( + ... text='hello', + ... start=(0, 0, 0), + ... forward=(1, 0, 0), + ... up=(0, 1, 0), + ... ) + >>> model = sf.make_3d_model([hello_banner]) + >>> assert model.html_viewer() is not None + """ + def __init__( self, *, text: str, - start: Sequence[float], - forward: Sequence[float], - up: Sequence[float], + start: tuple[float, float, float] | Sequence[float], + forward: tuple[float, float, float] | Sequence[float], + up: tuple[float, float, float] | Sequence[float], mirror_backside: bool = True, ): """Describes a rectangle showing text. @@ -29,12 +46,14 @@ def __init__( This is the `bottom_left` of the rectangle, in 3d. forward: The 3d direction along which the text grows as the message gets longer. This is the `bottom_right - bottom_left` of the rectangle, in 3d. - The length of this vector is ignored. - up: A 3d direction along which the text is oriented. + The length of this vector is ignored; the length of the rectangle is determined + automatically from the desired text. + up: The 3d direction along which the text is oriented. This is the `top_left - bottom_left` of the rectangle, in 3d. - The length of this vector is ignored. + The length of this vector is ignored; the height of the rectangle is determined + automatically from the text. Should be perpendicular to `forward`. - mirror_backside: Determines whether or not the text on the back of the rectangle + mirror_backside: Determines whether the text on the back of the rectangle is mirrored (making it readable) or not (keeping the forward direction consistent). Defaults to True (readable on both sides). """ @@ -45,8 +64,23 @@ def __init__( self.mirror_backside = mirror_backside -class TriangleData: - def __init__(self, *, rgba: tuple[float, float, float, float], triangle_list: np.ndarray): +class TriangleDataFor3DModel: + """Coordinates and colors of triangles to draw in a 3d model. + + Example: + >>> import stimflow as sf + >>> red_square = sf.TriangleDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... triangle_list=[ + ... # A square made of two triangles. + ... [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + ... [(1, 1, 0), (0, 1, 0), (1, 0, 0)], + ... ], + ... ) + >>> model = sf.make_3d_model([red_square]) + >>> assert model.html_viewer() is not None + """ + def __init__(self, *, rgba: tuple[float, float, float, float], triangle_list: np.ndarray | Iterable[Sequence[Sequence[float]]]): """Triangles with associated color information. Args: @@ -57,12 +91,25 @@ def __init__(self, *, rgba: tuple[float, float, float, float], triangle_list: np Axis 0 is the triangle axis (each entry is a triangle). Axis 1 is the ABC vertex axis (each entry is a vertex from the triangle). Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). + + Example: + >>> import stimflow as sf + >>> red_square = sf.TriangleDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... triangle_list=[ + ... # A square made of two triangles. + ... [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + ... [(1, 1, 0), (0, 1, 0), (1, 0, 0)], + ... ], + ... ) + >>> model = sf.make_3d_model([red_square]) + >>> assert model.html_viewer() is not None """ + triangle_list = np.asarray(triangle_list, dtype=np.float32) assert ( len(triangle_list.shape) == 3 and triangle_list.shape[1] == 3 and triangle_list.shape[2] == 3 - and triangle_list.dtype == np.float32 ) assert len(rgba) == 4 assert triangle_list.shape[0] > 0 @@ -76,7 +123,7 @@ def rect( origin: Iterable[float], d1: Iterable[float], d2: Iterable[float], - ) -> TriangleData: + ) -> TriangleDataFor3DModel: """Creates a pair of triangles forming a rectangle. Args: @@ -85,21 +132,21 @@ def rect( d1: The right - left displacement. d2: The top - bottom displacement. """ - origin = np.array(origin, dtype=np.float32) + np_origin = np.array(origin, dtype=np.float32) d1 = np.array(d1, dtype=np.float32) d2 = np.array(d2, dtype=np.float32) - p1 = origin + d1 - p2 = origin + d2 - return TriangleData( + p1 = np_origin + d1 + p2 = np_origin + d2 + return TriangleDataFor3DModel( rgba=rgba, triangle_list=np.array([ - [origin, p1, p2], + [np_origin, p1, p2], [p2, p1, p1 + d2], ], dtype=np.float32) ) @staticmethod - def fused(data: Iterable[TriangleData]) -> list[TriangleData]: + def fused(data: Iterable[TriangleDataFor3DModel]) -> list[TriangleDataFor3DModel]: """Attempts to combine triangle data instances into fewer instances.""" groups = collections.defaultdict(list) for e in data: @@ -110,7 +157,7 @@ def fused(data: Iterable[TriangleData]) -> list[TriangleData]: result.append(group[0]) else: result.append( - TriangleData( + TriangleDataFor3DModel( rgba=rgba, triangle_list=np.concatenate([e.triangle_list for e in group], axis=0), ) @@ -118,8 +165,26 @@ def fused(data: Iterable[TriangleData]) -> list[TriangleData]: return result -class LineData: - def __init__(self, *, rgba: tuple[float, float, float, float], edge_list: np.ndarray): +class LineDataFor3DModel: + """Coordinates and colors of lines to draw in a 3d model. + + Example: + >>> import stimflow as sf + >>> red_square_outline = sf.LineDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... edge_list=[ + ... # A square made of four lines. + ... [(0, 0, 0), (0, 1, 0)], + ... [(0, 1, 0), (1, 1, 0)], + ... [(1, 1, 0), (1, 0, 0)], + ... [(1, 0, 0), (0, 0, 0)], + ... ], + ... ) + >>> model = sf.make_3d_model([red_square_outline]) + >>> assert model.html_viewer() is not None + """ + + def __init__(self, *, rgba: tuple[float, float, float, float], edge_list: np.ndarray | Iterable[Sequence[Sequence[float]]]): """Lines with associated color information. Args: @@ -131,19 +196,18 @@ def __init__(self, *, rgba: tuple[float, float, float, float], edge_list: np.nda Axis 1 is the AB vertex axis (each entry is a vertex from the edge). Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). """ + np_edge_list = np.asarray(edge_list, dtype=np.float32) assert ( - len(edge_list.shape) == 3 - and edge_list.shape[1] == 2 - and edge_list.shape[2] == 3 - and edge_list.dtype == np.float32 + len(np_edge_list.shape) == 3 + and np_edge_list.shape[1] == 2 + and np_edge_list.shape[2] == 3 ) assert len(rgba) == 4 - assert edge_list.shape[0] > 0 self.rgba: tuple[float, float, float, float] = cast(Any, tuple(rgba)) - self.edge_list: np.ndarray = edge_list + self.edge_list: np.ndarray = np_edge_list @staticmethod - def fused(data: Iterable[LineData]) -> list[LineData]: + def fused(data: Iterable[LineDataFor3DModel]) -> list[LineDataFor3DModel]: """Attempts to combine line data instances into fewer instances.""" groups = collections.defaultdict(list) for e in data: @@ -154,7 +218,7 @@ def fused(data: Iterable[LineData]) -> list[LineData]: result.append(group[0]) else: result.append( - LineData( + LineDataFor3DModel( rgba=rgba, edge_list=np.concatenate([e.edge_list for e in group], axis=0) ) ) @@ -162,13 +226,13 @@ def fused(data: Iterable[LineData]) -> list[LineData]: def make_3d_model( - elements: Iterable[TriangleData | LineData | TextData], + elements: Iterable[TriangleDataFor3DModel | LineDataFor3DModel | TextDataFor3DModel], ) -> Viewable3dModelGLTF: - """Creates a 3d model containing the elements. + """Creates a 3d model containing the given elements. Args: elements: A list of objects to include in the model. The list can include triangles - (TriangleData), lines (LineData), and text (TextData). + (TriangleDataFor3DModel), lines (LineDataFor3DModel), and text (TextDataFor3DModel). Returns: The 3d model, as a `stimflow.gltf_model`. @@ -176,24 +240,70 @@ def make_3d_model( `stimflow.gltf_model` inherits from `pygltflib.GLTF2` but adds a `_repr_html_` class (creating a 3d viewer in Jupyter notebooks) and a `write_viewer_to` method for saving a standalone HTML viewer. + + Example: + >>> import stimflow as sf + >>> red_square = sf.TriangleDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... triangle_list=[ + ... # A square made of two triangles. + ... [(0, 0, 0), (0, 1, 0), (1, 0, 0)], + ... [(1, 1, 0), (0, 1, 0), (1, 0, 0)], + ... ], + ... ) + >>> blue_square_outline = sf.LineDataFor3DModel( + ... rgba=(0, 0, 1, 1), + ... edge_list=[ + ... # A square made of four lines. + ... [(0, 0, 2), (0, 1, 2)], + ... [(0, 1, 2), (1, 1, 2)], + ... [(1, 1, 2), (1, 0, 2)], + ... [(1, 0, 2), (0, 0, 2)], + ... ], + ... ) + >>> hello_banner = sf.TextDataFor3DModel( + ... text='hello', + ... start=(0, 0, 5), + ... forward=(1, 0, 0), + ... up=(0, 1, 0), + ... ) + >>> model: sf.Viewable3dModelGLTF = sf.make_3d_model([ + ... red_square, + ... hello_banner, + ... blue_square_outline, + ... ]) + >>> viewer: sf.str_html = model.html_viewer() + >>> + >>> # This line is commented out so that running doctest doesn't create a file + >>> # The 'write_to' method writes a file and also announces the written file:// URL to stderr. + >>> # viewer.write_to('tmp.html') + >>> + >>> print(viewer[:162] + "...") + + + + + + + str_html: model_data_uri = f"""data:text/plain;base64,{base64.b64encode(model_bytes).decode()}""" return str_html( - r''' - + r''' diff --git a/glue/stimflow/src/stimflow/_viz/__init__.py b/glue/stimflow/src/stimflow/_viz/__init__.py index ef6ece12..da3eb959 100644 --- a/glue/stimflow/src/stimflow/_viz/__init__.py +++ b/glue/stimflow/src/stimflow/_viz/__init__.py @@ -4,9 +4,9 @@ ) from stimflow._viz._viz_circuit_html import stim_circuit_html_viewer from stimflow._viz._3d_model import ( - LineData, - TriangleData, - TextData, + LineDataFor3DModel, + TriangleDataFor3DModel, + TextDataFor3DModel, make_3d_model, ) from stimflow._viz._viz_svg import svg From 34e85bcdd1d9947d49a8a8a21810440e95e38114 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 15:20:18 -0700 Subject: [PATCH 3/9] More examples, some API cuts --- .github/workflows/ci.yml | 2 +- dev/doctest_proper.py | 3 + glue/stimflow/README.md | 26 +++- glue/stimflow/doc/api.md | 145 ++++++++---------- glue/stimflow/src/stimflow/__init__.py | 3 +- glue/stimflow/src/stimflow/_chunk/__init__.py | 3 +- glue/stimflow/src/stimflow/_chunk/_chunk.py | 110 +++++++++---- .../src/stimflow/_chunk/_chunk_builder.py | 38 ++--- .../stimflow/_chunk/_chunk_builder_test.py | 4 +- .../src/stimflow/_chunk/_chunk_interface.py | 2 +- .../src/stimflow/_chunk/_chunk_loop.py | 25 +-- .../src/stimflow/_chunk/_chunk_test.py | 30 ++++ .../src/stimflow/_chunk/_code_util.py | 16 +- .../src/stimflow/_chunk/_code_util_test.py | 55 ++++--- .../src/stimflow/_chunk/_stabilizer_code.py | 16 +- .../stimflow/_chunk/_stabilizer_code_test.py | 12 +- glue/stimflow/src/stimflow/_core/_flow.py | 6 - glue/stimflow/src/stimflow/_core/_noise.py | 16 +- .../stimflow/src/stimflow/_core/_pauli_map.py | 108 +++++++++---- 19 files changed, 383 insertions(+), 237 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1557c77..219fbc39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -480,7 +480,7 @@ jobs: - run: pip install -e glue/stimflow - run: pip install pytest - run: pytest glue/stimflow - - run: dev/doctest_proper.py --module stimflow --suppress_examples_warning_for stimflow.str_svg stimflow.str_html + - run: dev/doctest_proper.py --module stimflow --suppress_examples_warning_for stimflow.str_svg stimflow.str_html stimflow.Viewable3dModelGLTF test_sinter: runs-on: ubuntu-24.04 steps: diff --git a/dev/doctest_proper.py b/dev/doctest_proper.py index b5a49b28..de9fb839 100755 --- a/dev/doctest_proper.py +++ b/dev/doctest_proper.py @@ -25,6 +25,9 @@ '__doc__', '__loader__', '__file__', + '__firstlineno__', + '__static_attributes__', + '__match_args__', } diff --git a/glue/stimflow/README.md b/glue/stimflow/README.md index 45bd273b..55490c44 100644 --- a/glue/stimflow/README.md +++ b/glue/stimflow/README.md @@ -2,17 +2,27 @@ stimflow: annealed utilities for creating QEC circuits ================================================= stimflow is a library for creating quantum error correction circuits. -In particular, stimflow decomposes the circuit creation problem into making and combining *chunks*. -A *chunk* is a circuit combined with stabilizer flow assertions the circuit is supposed to satisfy. -stimflow provides tools for making chunks, verifying chunks, debugging chunks, and compiling chunks into a complete final circuit. -If you're getting started, see stimflow's [getting started notebook](doc/getting_started.ipynb). +stimflow's design philosophy is to be a tool box, not a black box. +For example, stimflow does *not* include a `make_surface_code` method. +Instead it provides tools that can be used to more easily create a surface code circuit from scratch. +The hope is that these tools then make it easier to create as-yet-unknown constructions in the future. -See stimflow's [API reference](doc/api.md) for the suite available methods and types. +stimflow decomposes the circuit creation problem into making and combining *chunks*. +A *Chunk* is a circuit combined with stabilizer flow assertions that the circuit is supposed to satisfy. +stimflow provides tools for making chunks (`stimflow.ChunkBuilder`), verifying chunks (`stimflow.Chunk.verify`), debugging chunks (`stimflow.Chunk.to_html_viewer`), and compiling sequences of chunks into a complete final circuit (`stimflow.ChunkCompiler`). -stimflow's design philosophy is to be a tool box, not a black box. -For example, stimflow does not include methods for creating surface code circuits or other standard codes. -Instead it provides tools that can be used to more easily create those circuits. +stimflow also includes functionality for: + +- Transpiling (`stimflow.transpile_to_z_basis_interaction_circuit(...)`) +- Adding Noise (`stimflow.NoiseModel.uniform_depolarizing(p).noisy_circuit(...)`) +- Visualizing (`stimflow.make_3d_model`, `stimflow.stim_circuit_html_viewer`) + +# Documentation + +See stimflow's [getting started notebook](doc/getting_started.ipynb). + +See stimflow's [API reference](doc/api.md). # Backwards Compatibility Warning diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 1ff1c0dc..8addcd11 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -19,8 +19,7 @@ - [`stimflow.Chunk.to_coord_circuit`](#stimflow.Chunk.to_coord_circuit) - [`stimflow.Chunk.to_html_viewer`](#stimflow.Chunk.to_html_viewer) - [`stimflow.Chunk.verify`](#stimflow.Chunk.verify) - - [`stimflow.Chunk.verify_distance_is_at_least_2`](#stimflow.Chunk.verify_distance_is_at_least_2) - - [`stimflow.Chunk.verify_distance_is_at_least_3`](#stimflow.Chunk.verify_distance_is_at_least_3) + - [`stimflow.Chunk.verify_distance_is_at_least`](#stimflow.Chunk.verify_distance_is_at_least) - [`stimflow.Chunk.with_edits`](#stimflow.Chunk.with_edits) - [`stimflow.Chunk.with_flag_added_to_all_flows`](#stimflow.Chunk.with_flag_added_to_all_flows) - [`stimflow.Chunk.with_obs_flows_as_det_flows`](#stimflow.Chunk.with_obs_flows_as_det_flows) @@ -73,8 +72,7 @@ - [`stimflow.ChunkLoop.to_closed_circuit`](#stimflow.ChunkLoop.to_closed_circuit) - [`stimflow.ChunkLoop.to_html_viewer`](#stimflow.ChunkLoop.to_html_viewer) - [`stimflow.ChunkLoop.verify`](#stimflow.ChunkLoop.verify) - - [`stimflow.ChunkLoop.verify_distance_is_at_least_2`](#stimflow.ChunkLoop.verify_distance_is_at_least_2) - - [`stimflow.ChunkLoop.verify_distance_is_at_least_3`](#stimflow.ChunkLoop.verify_distance_is_at_least_3) + - [`stimflow.ChunkLoop.verify_distance_is_at_least`](#stimflow.ChunkLoop.verify_distance_is_at_least) - [`stimflow.ChunkLoop.with_repetitions`](#stimflow.ChunkLoop.with_repetitions) - [`stimflow.ChunkReflow`](#stimflow.ChunkReflow) - [`stimflow.ChunkReflow.end_code`](#stimflow.ChunkReflow.end_code) @@ -216,8 +214,7 @@ - [`stimflow.StabilizerCode.transversal_measure_chunk`](#stimflow.StabilizerCode.transversal_measure_chunk) - [`stimflow.StabilizerCode.used_set`](#stimflow.StabilizerCode.used_set) - [`stimflow.StabilizerCode.verify`](#stimflow.StabilizerCode.verify) - - [`stimflow.StabilizerCode.verify_distance_is_at_least_2`](#stimflow.StabilizerCode.verify_distance_is_at_least_2) - - [`stimflow.StabilizerCode.verify_distance_is_at_least_3`](#stimflow.StabilizerCode.verify_distance_is_at_least_3) + - [`stimflow.StabilizerCode.verify_distance_is_at_least`](#stimflow.StabilizerCode.verify_distance_is_at_least) - [`stimflow.StabilizerCode.with_edits`](#stimflow.StabilizerCode.with_edits) - [`stimflow.StabilizerCode.with_integer_coordinates`](#stimflow.StabilizerCode.with_integer_coordinates) - [`stimflow.StabilizerCode.with_observables_from_basis`](#stimflow.StabilizerCode.with_observables_from_basis) @@ -276,8 +273,7 @@ - [`stimflow.svg`](#stimflow.svg) - [`stimflow.transpile_to_z_basis_interaction_circuit`](#stimflow.transpile_to_z_basis_interaction_circuit) - [`stimflow.transversal_code_transition_chunks`](#stimflow.transversal_code_transition_chunks) -- [`stimflow.verify_distance_is_at_least_2`](#stimflow.verify_distance_is_at_least_2) -- [`stimflow.verify_distance_is_at_least_3`](#stimflow.verify_distance_is_at_least_3) +- [`stimflow.verify_distance_is_at_least`](#stimflow.verify_distance_is_at_least) - [`stimflow.xor_sorted`](#stimflow.xor_sorted) ```python # Types used by the method definitions. @@ -543,33 +539,18 @@ def verify( """ ``` - + ```python -# stimflow.Chunk.verify_distance_is_at_least_2 +# stimflow.Chunk.verify_distance_is_at_least # (in class stimflow.Chunk) -def verify_distance_is_at_least_2( +def verify_distance_is_at_least( self, + minimum_distance: int, *, noise: float | NoiseModel = 0.001, ): - """Verifies undetected logical errors require at least 2 physical errors. - - By default, verifies using a uniform depolarizing circuit noise model. - """ -``` - - -```python -# stimflow.Chunk.verify_distance_is_at_least_3 - -# (in class stimflow.Chunk) -def verify_distance_is_at_least_3( - self, - *, - noise: float | NoiseModel = 0.001, -): - """Verifies undetected logical errors require at least 3 physical errors. + """Verifies undetected logical errors require at least the given number of physical errors. By default, verifies using a uniform depolarizing circuit noise model. """ @@ -1405,38 +1386,23 @@ def verify( ): ``` - + ```python -# stimflow.ChunkLoop.verify_distance_is_at_least_2 +# stimflow.ChunkLoop.verify_distance_is_at_least # (in class stimflow.ChunkLoop) -def verify_distance_is_at_least_2( +def verify_distance_is_at_least( self, + minimum_distance: int, *, noise: float | NoiseModel = 0.001, ): - """Verifies undetected logical errors require at least 2 physical errors. + """Verifies undetected logical errors require at least the given number of physical errors. Verifies using a uniform depolarizing circuit noise model. """ ``` - -```python -# stimflow.ChunkLoop.verify_distance_is_at_least_3 - -# (in class stimflow.ChunkLoop) -def verify_distance_is_at_least_3( - self, - *, - noise: float | NoiseModel = 0.001, -): - """Verifies undetected logical errors require at least 3 physical errors. - - By default, verifies using a uniform depolarizing circuit noise model. - """ -``` - ```python # stimflow.ChunkLoop.with_repetitions @@ -2636,7 +2602,21 @@ def with_xz_flipped( # (at top-level in the stimflow module) class PauliMap: - """A qubit-to-pauli mapping. + """An immutable qubit-to-pauli mapping. + + Similar to a stim.PauliString, but sparse instead of dense and also PauliMap + doesn't track signs (i.e. X*Y produces Z instead of i*Z). + + The mapping can also be given a name. In some contexts, stimflow requires that Pauli mappings + have a name (e.g. when specifying the Pauli mapping of a logical operator for a stabilizer code). + + Examples: + >>> import stimflow as sf + >>> p1 = sf.PauliMap({0: "X", 1: "Y", 2: "Z"}) + >>> p2 = sf.PauliMap.from_xs([1, 2, 3]) + >>> p3 = sf.PauliMap({"Z": [3, 4j]}) + >>> print(p1 * p2 * p3) + X0*Z4j*Z1*Y2*Y3 """ ``` @@ -2658,6 +2638,31 @@ def __init__( name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, in order to identify the Pauli map. A common convention used in the library is that named Pauli maps correspond to logical operators. + + Examples: + >>> import stimflow as sf + >>> import stim + + >>> print(sf.PauliMap()) + I + + >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"})) + X0*Y1*Z2 + + >>> print(sf.PauliMap({"X": [1, 2], "Y": 1+1j})) + X1*Y(1+1j)*X2 + + >>> print(sf.PauliMap(stim.PauliString("XYZ_X"))) + X0*Y1*Z2*X4 + + >>> print(sf.PauliMap(sf.Tile(data_qubits=[1, 2, 3], bases="X"))) + X1*X2*X3 + + >>> print(sf.PauliMap({0: "X", "Y": [0, 1]})) + Z0*Y1 + + >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"}, name="test")) + (name='test') X0*Y1*Z2 """ ``` @@ -3364,32 +3369,15 @@ def verify( """ ``` - -```python -# stimflow.StabilizerCode.verify_distance_is_at_least_2 - -# (in class stimflow.StabilizerCode) -def verify_distance_is_at_least_2( - self, -): - """Verifies undetected logical errors require at least 2 physical errors. - - Verifies using a code capacity noise model. - """ -``` - - + ```python -# stimflow.StabilizerCode.verify_distance_is_at_least_3 +# stimflow.StabilizerCode.verify_distance_is_at_least # (in class stimflow.StabilizerCode) -def verify_distance_is_at_least_3( +def verify_distance_is_at_least( self, + minimum_distance: int, ): - """Verifies undetected logical errors require at least 3 physical errors. - - Verifies using a code capacity noise model. - """ ``` @@ -4476,23 +4464,14 @@ def transversal_code_transition_chunks( ) -> tuple[Chunk, ChunkReflow, Chunk]: ``` - -```python -# stimflow.verify_distance_is_at_least_2 - -# (at top-level in the stimflow module) -def verify_distance_is_at_least_2( - obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode, -): -``` - - + ```python -# stimflow.verify_distance_is_at_least_3 +# stimflow.verify_distance_is_at_least # (at top-level in the stimflow module) -def verify_distance_is_at_least_3( +def verify_distance_is_at_least( obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode, + minimum_distance: int, ): ``` diff --git a/glue/stimflow/src/stimflow/__init__.py b/glue/stimflow/src/stimflow/__init__.py index bb44c84f..0051b073 100644 --- a/glue/stimflow/src/stimflow/__init__.py +++ b/glue/stimflow/src/stimflow/__init__.py @@ -16,8 +16,7 @@ StabilizerCode, StimCircuitLoom, transversal_code_transition_chunks, - verify_distance_is_at_least_2, - verify_distance_is_at_least_3, + verify_distance_is_at_least, ) from stimflow._core import ( append_reindexed_content_to_circuit, diff --git a/glue/stimflow/src/stimflow/_chunk/__init__.py b/glue/stimflow/src/stimflow/_chunk/__init__.py index c53b361f..5e27ff65 100644 --- a/glue/stimflow/src/stimflow/_chunk/__init__.py +++ b/glue/stimflow/src/stimflow/_chunk/__init__.py @@ -11,8 +11,7 @@ find_d1_error, find_d2_error, transversal_code_transition_chunks, - verify_distance_is_at_least_2, - verify_distance_is_at_least_3, + verify_distance_is_at_least, ) from stimflow._chunk._flow_metadata import FlowMetadata from stimflow._chunk._patch import Patch diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk.py b/glue/stimflow/src/stimflow/_chunk/_chunk.py index f066716f..b6cdbe67 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk.py @@ -7,8 +7,7 @@ import stim from stimflow._chunk._code_util import ( - verify_distance_is_at_least_2, - verify_distance_is_at_least_3, + verify_distance_is_at_least, ) from stimflow._chunk._patch import Patch from stimflow._chunk._stabilizer_code import StabilizerCode @@ -31,7 +30,27 @@ class Chunk: - """A circuit chunk with accompanying stabilizer flow assertions.""" + """A circuit with accompanying stabilizer flow assertions. + + This object is intended to be immutable. + Some of its fields are editable types, but it is assumed they do not change + (e.g. computations may be cached). + Don't do things like appending to the circuit of a chunk after the chunk is created. + + Example: + >>> import stimflow as sf + >>> import stim + >>> chunk = sf.Chunk( + ... circuit=stim.Circuit(''' + ... QUBIT_COORDS(1, 2) 0 + ... H 0 + ... '''), + ... flows=[ + ... sf.Flow(start=sf.PauliMap({1+2j: "X"}), end=sf.PauliMap({1+2j: "Z"})), + ... ], + ... ) + >>> chunk.verify() + """ def __init__( self, @@ -45,7 +64,7 @@ def __init__( q2i: dict[complex, int] | None = None, o2i: dict[Any, int] | None = None, ): - """ + """Creates a `stimflow.Chunk` with the given values. Args: circuit: The circuit implementing the chunk's functionality. @@ -64,16 +83,32 @@ def __init__( flowing in. wants_to_merge_with_next: Defaults to False. When set to True, the chunk compiler won't insert a TICK between this chunk - and the next chunk. + and the next chunk. For example, this is useful when creating a + transversal initialization chunk. wants_to_merge_with_prev: Defaults to False. When set to True, the chunk compiler won't insert a TICK between this chunk - and the previous chunk. + and the previous chunk. For example, this is useful when creating a + transversal measurement chunk. q2i: Defaults to None (infer from QUBIT_COORDS instructions in circuit else raise an exception). The stimflow-qubit-coordinate-to-stim-qubit-index mapping used to translate between stimflow's qubit keys and stim's qubit keys. o2i: Defaults to None (raise an exception if observables present in circuit). The stimflow-observable-key-to-stim-observable-index mapping used to translate between stimflow's observable keys and stim's observable keys. + + Example: + >>> import stimflow as sf + >>> import stim + >>> chunk = sf.Chunk( + ... circuit=stim.Circuit(''' + ... QUBIT_COORDS(1, 2) 0 + ... H 0 + ... '''), + ... flows=[ + ... sf.Flow(start=sf.PauliMap({1+2j: "X"}), end=sf.PauliMap({1+2j: "Z"})), + ... ], + ... ) + >>> chunk.verify() """ if q2i is None: q2i = {x + 1j * y: i for i, (x, y) in circuit.get_final_qubit_coordinates().items()} @@ -522,35 +557,58 @@ def to_closed_circuit(self) -> stim.Circuit: compiler.append_magic_end_chunk(self.end_interface()) return compiler.finish_circuit() - def verify_distance_is_at_least_2(self, *, noise: float | NoiseModel = 1e-3): - """Verifies undetected logical errors require at least 2 physical errors. - - By default, verifies using a uniform depolarizing circuit noise model. - """ - __tracebackhide__ = True - circuit = self.to_closed_circuit() - if isinstance(noise, float): - noise = NoiseModel.uniform_depolarizing(1e-3) - circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) - verify_distance_is_at_least_2(circuit) - def to_coord_circuit(self) -> stim.Circuit: coords = stim.Circuit() for q, i in self.q2i.items(): coords.append("QUBIT_COORDS", [i], [q.real, q.imag]) return coords + self.circuit - def verify_distance_is_at_least_3(self, *, noise: float | NoiseModel = 1e-3): - """Verifies undetected logical errors require at least 3 physical errors. + def verify_distance_is_at_least(self, minimum_distance: int, *, noise: float | NoiseModel = 1e-3): + """Verifies undetected logical errors require at least the given number of physical errors. - By default, verifies using a uniform depolarizing circuit noise model. + Args: + minimum_distance: The minimum distance to verify. Currently this must be at most 3. + noise: The noise model to use. Defaults to a uniform depolarizing circuit noise model + that allows multiple operations per tick and where two qubit gates apply two qubit + depolarizing noise. + + Example: + >>> import stimflow as sf + >>> import stim + >>> lz = sf.PauliMap({0: "Z"}).with_name("LZ") + >>> zz01 = sf.PauliMap.from_zs([0, 1]) + >>> zz12 = sf.PauliMap.from_zs([1, 2]) + >>> zz23 = sf.PauliMap.from_zs([2, 3]) + >>> zz34 = sf.PauliMap.from_zs([3, 4]) + >>> chunk = sf.Chunk( + ... stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(1, 0) 1 + ... QUBIT_COORDS(2, 0) 2 + ... QUBIT_COORDS(3, 0) 3 + ... QUBIT_COORDS(4, 0) 4 + ... MZZ 0 1 1 2 2 3 3 4 + ... '''), + ... flows=[ + ... sf.Flow(start=lz, end=lz), + ... sf.Flow(start=zz01, mids=[0]), + ... sf.Flow(start=zz12, mids=[1]), + ... sf.Flow(start=zz23, mids=[2]), + ... sf.Flow(start=zz34, mids=[3]), + ... sf.Flow(end=zz01, mids=[0]), + ... sf.Flow(end=zz12, mids=[1]), + ... sf.Flow(end=zz23, mids=[2]), + ... sf.Flow(end=zz34, mids=[3]), + ... ], + ... ) + >>> chunk.verify_distance_is_at_least(3) """ __tracebackhide__ = True circuit = self.to_closed_circuit() if isinstance(noise, float): - noise = NoiseModel.uniform_depolarizing(1e-3) + noise = NoiseModel.uniform_depolarizing(1e-3, allow_multiple_uses_of_a_qubit_in_one_tick=True) circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) - verify_distance_is_at_least_3(circuit) + verify_distance_is_at_least(circuit, minimum_distance) def find_logical_error( self, @@ -563,7 +621,7 @@ def find_logical_error( circuit = self.to_closed_circuit() if not skip_adding_noise: if isinstance(noise, float): - noise = NoiseModel.uniform_depolarizing(1e-3) + noise = NoiseModel.uniform_depolarizing(1e-3, allow_multiple_uses_of_a_qubit_in_one_tick=True) circuit = noise.noisy_circuit_skipping_mpp_boundaries( circuit, immune_qubit_coords=noiseless_qubits ) @@ -727,9 +785,9 @@ def time_reversed(self) -> Chunk: for flow in self.flows: inp = stim.PauliString(len(self.q2i)) out = stim.PauliString(len(self.q2i)) - for q, p in flow.start.qubits.items(): + for q, p in flow.start.items(): inp[self.q2i[q]] = p - for q, p in flow.end.qubits.items(): + for q, p in flow.end.items(): out[self.q2i[q]] = p stim_flows.append( stim.Flow(input=inp, output=out, measurements=cast(Any, flow.measurement_indices)) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py index 20497af8..1072c158 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py @@ -38,12 +38,12 @@ def __init__( self.circuit: stim.Circuit = stim.Circuit() self.q2i: dict[complex, int] = {} self.o2i: dict[Any, int] = {} - self.flows: list[Flow] = [] - self.flows_with_auto_ms: list[Flow] = [] - self.flows_with_auto_start: list[Flow] = [] - self.flows_with_auto_end: list[Flow] = [] - self.discarded_output_flows: list[PauliMap] = [] - self.discarded_input_flows: list[PauliMap] = [] + self._flows: list[Flow] = [] + self._flows_with_auto_ms: list[Flow] = [] + self._flows_with_auto_start: list[Flow] = [] + self._flows_with_auto_end: list[Flow] = [] + self._discarded_output_flows: list[PauliMap] = [] + self._discarded_input_flows: list[PauliMap] = [] # Index allowed qubits. if allowed_qubits is not None: @@ -162,12 +162,12 @@ def lookup_mids(self, keys: Iterable[Any], *, ignore_unmatched: bool = False) -> def add_discarded_flow_input(self, flow: PauliMap | Tile) -> None: if isinstance(flow, Tile): flow = flow.to_pauli_map() - self.discarded_input_flows.append(flow) + self._discarded_input_flows.append(flow) def add_discarded_flow_output(self, flow: PauliMap | Tile) -> None: if isinstance(flow, Tile): flow = flow.to_pauli_map() - self.discarded_output_flows.append(flow) + self._discarded_output_flows.append(flow) def add_flow( self, @@ -233,15 +233,15 @@ def add_flow( f" {start=}" f" {ms=}" f" {end=}") - out = self.flows + out = self._flows if start == "auto": - out = self.flows_with_auto_start + out = self._flows_with_auto_start start = None elif end == "auto": - out = self.flows_with_auto_end + out = self._flows_with_auto_end end = None elif ms == "auto": - out = self.flows_with_auto_ms + out = self._flows_with_auto_ms ms = () out.append( @@ -274,19 +274,19 @@ def finish_chunk( end_fails = [] solved_starts = _solve_auto_flow_starts( - flows=self.flows_with_auto_start, + flows=self._flows_with_auto_start, circuit=self.circuit, q2i=self.q2i, failure_out=start_fails, ) solved_ends = _solve_auto_flow_ends( - flows=self.flows_with_auto_end, + flows=self._flows_with_auto_end, circuit=self.circuit, q2i=self.q2i, failure_out=end_fails, ) solved_ms = _solve_auto_flow_ms( - flows=self.flows_with_auto_ms, + flows=self._flows_with_auto_ms, circuit=self.circuit, q2i=self.q2i, o2i=self.o2i, @@ -350,9 +350,9 @@ def finish_chunk( circuit=out_circuit, q2i=self.q2i, o2i=self.o2i, - flows=self.flows + solved_starts + solved_ms + solved_ends, - discarded_inputs=self.discarded_input_flows, - discarded_outputs=self.discarded_output_flows, + flows=self._flows + solved_starts + solved_ms + solved_ends, + discarded_inputs=self._discarded_input_flows, + discarded_outputs=self._discarded_output_flows, wants_to_merge_with_next=wants_to_merge_with_next, wants_to_merge_with_prev=wants_to_merge_with_prev, ) @@ -488,7 +488,7 @@ def append( arg = self._ensure_obs_index_of(targets.name) self._ensure_indices( - targets.qubits, + targets.keys(), context_gate=gate, context_targets=targets, context_arg=arg, diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py index 80bb052d..5891892d 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py @@ -255,8 +255,8 @@ def test_partial_observable_include_memory_experiment(): builder_end.add_discarded_flow_input(stab_z0) builder_end.add_discarded_flow_input(stab_z1) builder_end.add_flow(start=obs_z) - builder_end.add_flow(start=stab_x, ms=stab_x.qubits) - builder_end.add_flow(start=obs_x, ms=obs_x.qubits) + builder_end.add_flow(start=stab_x, ms=stab_x.keys()) + builder_end.add_flow(start=obs_x, ms=obs_x.keys()) chunk_end = builder_end.finish_chunk() chunk_init.verify() diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py b/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py index 729f1398..0aec0f49 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py @@ -141,7 +141,7 @@ def data_set(self) -> frozenset[complex]: q for pauli_string_list in [self.ports, self.discards] for ps in pauli_string_list - for q in ps.qubits + for q in ps ) def to_patch(self) -> Patch: diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py b/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py index c40b3b70..deb77425 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py @@ -6,8 +6,7 @@ import stim from stimflow._chunk._code_util import ( - verify_distance_is_at_least_2, - verify_distance_is_at_least_3, + verify_distance_is_at_least, ) from stimflow._chunk._patch import Patch from stimflow._chunk._stabilizer_code import StabilizerCode @@ -116,29 +115,17 @@ def to_closed_circuit(self) -> stim.Circuit: compiler.append_magic_end_chunk() return compiler.finish_circuit() - def verify_distance_is_at_least_2(self, *, noise: float | NoiseModel = 1e-3): - """Verifies undetected logical errors require at least 2 physical errors. + def verify_distance_is_at_least(self, minimum_distance: int, *, noise: float | NoiseModel = 1e-3): + """Verifies undetected logical errors require at least the given number of physical errors. Verifies using a uniform depolarizing circuit noise model. """ __tracebackhide__ = True circuit = self.to_closed_circuit() if isinstance(noise, float): - noise = NoiseModel.uniform_depolarizing(1e-3) + noise = NoiseModel.uniform_depolarizing(1e-3, allow_multiple_uses_of_a_qubit_in_one_tick=True) circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) - verify_distance_is_at_least_2(circuit) - - def verify_distance_is_at_least_3(self, *, noise: float | NoiseModel = 1e-3): - """Verifies undetected logical errors require at least 3 physical errors. - - By default, verifies using a uniform depolarizing circuit noise model. - """ - __tracebackhide__ = True - circuit = self.to_closed_circuit() - if isinstance(noise, float): - noise = NoiseModel.uniform_depolarizing(1e-3) - circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) - verify_distance_is_at_least_3(circuit) + verify_distance_is_at_least(circuit, minimum_distance) def find_logical_error( self, @@ -153,7 +140,7 @@ def find_logical_error( """ circuit = self.to_closed_circuit() if isinstance(noise, float): - noise = NoiseModel.uniform_depolarizing(1e-3) + noise = NoiseModel.uniform_depolarizing(1e-3, allow_multiple_uses_of_a_qubit_in_one_tick=True) circuit = noise.noisy_circuit_skipping_mpp_boundaries( circuit, immune_qubit_coords=noiseless_qubits ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py index 4d63d733..c8ab89e4 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py @@ -549,3 +549,33 @@ def test_embedded_observables(): o2i={"L2": 2}, ) chunk.verify() + + +def test_verify_distance(): + lz = stimflow.PauliMap({0: "Z"}).with_name("LZ") + zz01 = stimflow.PauliMap.from_zs([0, 1]) + zz12 = stimflow.PauliMap.from_zs([1, 2]) + zz23 = stimflow.PauliMap.from_zs([2, 3]) + zz34 = stimflow.PauliMap.from_zs([3, 4]) + chunk = stimflow.Chunk( + stim.Circuit(""" + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + QUBIT_COORDS(2, 0) 2 + QUBIT_COORDS(3, 0) 3 + QUBIT_COORDS(4, 0) 4 + MZZ 0 1 1 2 2 3 3 4 + """), + flows=[ + stimflow.Flow(start=lz, end=lz), + stimflow.Flow(start=zz01, mids=[0]), + stimflow.Flow(start=zz12, mids=[1]), + stimflow.Flow(start=zz23, mids=[2]), + stimflow.Flow(start=zz34, mids=[3]), + stimflow.Flow(end=zz01, mids=[0]), + stimflow.Flow(end=zz12, mids=[1]), + stimflow.Flow(end=zz23, mids=[2]), + stimflow.Flow(end=zz34, mids=[3]), + ], + ) + chunk.verify_distance_is_at_least(3) diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util.py b/glue/stimflow/src/stimflow/_chunk/_code_util.py index d6fcf638..a58395ee 100644 --- a/glue/stimflow/src/stimflow/_chunk/_code_util.py +++ b/glue/stimflow/src/stimflow/_chunk/_code_util.py @@ -135,7 +135,17 @@ def find_d2_error( return None -def verify_distance_is_at_least_2(obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode): +def verify_distance_is_at_least(obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode, minimum_distance: int): + if minimum_distance == 2: + _verify_distance_is_at_least_2(obj) + elif minimum_distance == 3: + _verify_distance_is_at_least_3(obj) + elif minimum_distance < 2: + return + else: + raise NotImplementedError("Only minimum_distance=2 and minimum_distance=3 are implemented efficiently.") + +def _verify_distance_is_at_least_2(obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode): __tracebackhide__ = True if isinstance(obj, StabilizerCode): obj.verify_distance_is_at_least_2() @@ -145,7 +155,7 @@ def verify_distance_is_at_least_2(obj: stim.Circuit | stim.DetectorErrorModel | raise ValueError(f"Found a distance 1 error: {err}") -def verify_distance_is_at_least_3(obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode): +def _verify_distance_is_at_least_3(obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode): __tracebackhide__ = True err = find_d2_error(obj) if err is not None: @@ -215,7 +225,7 @@ def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: reflow = ChunkReflow.from_auto_rewrite_transitions_using_stable( stable=[ cast(PauliMap, flow.start) - for flow in next_builder.flows + for flow in next_builder._flows if flow.obs_key is None if flow.start ], diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util_test.py b/glue/stimflow/src/stimflow/_chunk/_code_util_test.py index 79a84f63..88d24163 100644 --- a/glue/stimflow/src/stimflow/_chunk/_code_util_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_code_util_test.py @@ -5,17 +5,18 @@ def test_verify_distance_is_at_least_23(): - stimflow.verify_distance_is_at_least_2( + stimflow.verify_distance_is_at_least( stim.Circuit( """ R 0 X_ERROR(0.125) 0 M 0 """ - ) + ), + 2, ) - stimflow.verify_distance_is_at_least_2( + stimflow.verify_distance_is_at_least( stim.Circuit( """ R 0 @@ -24,10 +25,11 @@ def test_verify_distance_is_at_least_23(): DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """ - ) + ), + 2, ) - stimflow.verify_distance_is_at_least_2( + stimflow.verify_distance_is_at_least( stim.Circuit( """ R 0 @@ -36,11 +38,12 @@ def test_verify_distance_is_at_least_23(): DETECTOR rec[-1] OBSERVABLE_INCLUDE(0) rec[-1] """ - ).detector_error_model() + ).detector_error_model(), + 2, ) with pytest.raises(ValueError, match="distance 1 error"): - stimflow.verify_distance_is_at_least_2( + stimflow.verify_distance_is_at_least( stim.Circuit( """ R 0 @@ -48,11 +51,12 @@ def test_verify_distance_is_at_least_23(): M 0 OBSERVABLE_INCLUDE(0) rec[-1] """ - ) + ), + 2, ) with pytest.raises(ValueError, match="distance 1 error"): - stimflow.verify_distance_is_at_least_3( + stimflow.verify_distance_is_at_least( stim.Circuit( """ R 0 @@ -60,62 +64,69 @@ def test_verify_distance_is_at_least_23(): M 0 OBSERVABLE_INCLUDE(0) rec[-1] """ - ) + ), + 3, ) - stimflow.verify_distance_is_at_least_2( + stimflow.verify_distance_is_at_least( stim.Circuit.generated( code_task="repetition_code:memory", distance=2, rounds=3, after_clifford_depolarization=1e-3, - ) + ), + 2, ) - stimflow.verify_distance_is_at_least_2( + stimflow.verify_distance_is_at_least( stim.Circuit.generated( code_task="repetition_code:memory", distance=3, rounds=3, after_clifford_depolarization=1e-3, - ) + ), + minimum_distance=2, ) - stimflow.verify_distance_is_at_least_2( + stimflow.verify_distance_is_at_least( stim.Circuit.generated( code_task="repetition_code:memory", distance=9, rounds=3, after_clifford_depolarization=1e-3, - ) + ), + 2, ) - stimflow.verify_distance_is_at_least_3( + stimflow.verify_distance_is_at_least( stim.Circuit.generated( code_task="repetition_code:memory", distance=3, rounds=3, after_clifford_depolarization=1e-3, - ) + ), + 3, ) - stimflow.verify_distance_is_at_least_3( + stimflow.verify_distance_is_at_least( stim.Circuit.generated( code_task="repetition_code:memory", distance=9, rounds=3, after_clifford_depolarization=1e-3, - ) + ), + 3, ) with pytest.raises(ValueError, match="distance 2 error"): - stimflow.verify_distance_is_at_least_3( + stimflow.verify_distance_is_at_least( stim.Circuit.generated( code_task="repetition_code:memory", distance=2, rounds=3, after_clifford_depolarization=1e-3, - ) + ), + 3, ) diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py index f174ce57..e1ed957f 100644 --- a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py @@ -278,7 +278,17 @@ def tiles(self) -> tuple[stimflow.Tile, ...]: """Returns the tiles of the code's stabilizer patch.""" return self.stabilizers.tiles - def verify_distance_is_at_least_2(self): + def verify_distance_is_at_least(self, minimum_distance: int): + if minimum_distance == 2: + self._verify_distance_is_at_least_2() + elif minimum_distance == 3: + self._verify_distance_is_at_least_3() + elif minimum_distance < 2: + return + else: + raise NotImplementedError("Only minimum_distance=2 and minimum_distance=3 are implemented efficiently.") + + def _verify_distance_is_at_least_2(self): """Verifies undetected logical errors require at least 2 physical errors. Verifies using a code capacity noise model. @@ -308,13 +318,13 @@ def verify_distance_is_at_least_2(self): f"\n {loc.gate_target.pauli_type} at {loc.coords}" ) - def verify_distance_is_at_least_3(self): + def _verify_distance_is_at_least_3(self): """Verifies undetected logical errors require at least 3 physical errors. Verifies using a code capacity noise model. """ __tracebackhide__ = True - self.verify_distance_is_at_least_2() + self._verify_distance_is_at_least_2() seen = {} circuit = self.make_code_capacity_circuit(noise=1e-3) for inst in circuit.detector_error_model().flattened(): diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py index 9c157c02..17ec4e40 100644 --- a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py @@ -133,9 +133,9 @@ def test_verify_distance_is_at_least_3(): ], ) with pytest.raises(ValueError, match="distance 1 error"): - distance_1_code.verify_distance_is_at_least_2() + distance_1_code.verify_distance_is_at_least(2) with pytest.raises(ValueError, match="distance 1 error"): - distance_1_code.verify_distance_is_at_least_3() + distance_1_code.verify_distance_is_at_least(3) distance_2_code = stimflow.StabilizerCode( stabilizers=stimflow.Patch( @@ -151,9 +151,9 @@ def test_verify_distance_is_at_least_3(): ) ], ) - distance_2_code.verify_distance_is_at_least_2() + distance_2_code.verify_distance_is_at_least(2) with pytest.raises(ValueError, match="distance 2 error"): - distance_2_code.verify_distance_is_at_least_3() + distance_2_code.verify_distance_is_at_least(3) perfect_code = stimflow.StabilizerCode( stabilizers=stimflow.Patch( @@ -171,8 +171,8 @@ def test_verify_distance_is_at_least_3(): ) ], ) - perfect_code.verify_distance_is_at_least_2() - perfect_code.verify_distance_is_at_least_3() + perfect_code.verify_distance_is_at_least(2) + perfect_code.verify_distance_is_at_least(3) def test_with_integer_coordinates(): diff --git a/glue/stimflow/src/stimflow/_core/_flow.py b/glue/stimflow/src/stimflow/_core/_flow.py index 6faee9c9..eea10312 100644 --- a/glue/stimflow/src/stimflow/_core/_flow.py +++ b/glue/stimflow/src/stimflow/_core/_flow.py @@ -48,12 +48,6 @@ def __init__( "color" of the flow in a color code. sign: Defaults to None (unsigned). The expected sign of the flow. """ - if start == "auto": - raise ValueError(f"stimflow.Flow no longer supports {start=}. Use stimflow.FlowSemiAuto instead.") - if end == "auto": - raise ValueError(f"stimflow.Flow no longer supports {end=}. Use stimflow.FlowSemiAuto instead.") - if mids == "auto": - raise ValueError(f"stimflow.Flow no longer supports {mids=}. Use stimflow.FlowSemiAuto instead.") if obs_key is None and center is None: if isinstance(start, Tile) and start.measure_qubit is not None: center = start.measure_qubit diff --git a/glue/stimflow/src/stimflow/_core/_noise.py b/glue/stimflow/src/stimflow/_core/_noise.py index 5e47cb37..b23bb358 100644 --- a/glue/stimflow/src/stimflow/_core/_noise.py +++ b/glue/stimflow/src/stimflow/_core/_noise.py @@ -156,7 +156,12 @@ def si1000(p: float) -> NoiseModel: ) @staticmethod - def uniform_depolarizing(p: float, *, single_qubit_only: bool = False) -> NoiseModel: + def uniform_depolarizing( + p: float, + *, + single_qubit_only: bool = False, + allow_multiple_uses_of_a_qubit_in_one_tick: bool = False, + ) -> NoiseModel: """Near-standard circuit depolarizing noise. Everything has the same parameter p. @@ -167,6 +172,14 @@ def uniform_depolarizing(p: float, *, single_qubit_only: bool = False) -> NoiseM Non-demolition measurement is treated a bit unusually in that it is the result that is flipped instead of the input qubit. The input qubit is depolarized. + + Args: + single_qubit_only: Defaults to False. When False, two qubit gates apply two + qubit depolarizing noise (DEPOLARIZE2). When True, they instead apply single qubit + depolarizing noise (DEPOLARIZE1). + allow_multiple_uses_of_a_qubit_in_one_tick: Defaults to False. When False, an error will be + raised if attempting to add noise to a circuit that operates on a qubit + multiple times between TICK operations. When set to True, no error is raised. """ dep2 = "DEPOLARIZE1" if single_qubit_only else "DEPOLARIZE2" return NoiseModel( @@ -192,6 +205,7 @@ def uniform_depolarizing(p: float, *, single_qubit_only: bool = False) -> NoiseM "RY": NoiseRule(after={"X_ERROR": p}), "R": NoiseRule(after={"X_ERROR": p}), }, + allow_multiple_uses_of_a_qubit_in_one_tick=allow_multiple_uses_of_a_qubit_in_one_tick, ) def _noise_rule_for_split_operation( diff --git a/glue/stimflow/src/stimflow/_core/_pauli_map.py b/glue/stimflow/src/stimflow/_core/_pauli_map.py index 489ad115..8ef80119 100644 --- a/glue/stimflow/src/stimflow/_core/_pauli_map.py +++ b/glue/stimflow/src/stimflow/_core/_pauli_map.py @@ -23,7 +23,22 @@ class PauliMap: - """A qubit-to-pauli mapping.""" + """An immutable qubit-to-pauli mapping. + + Similar to a stim.PauliString, but sparse instead of dense and also PauliMap + doesn't track signs (i.e. X*Y produces Z instead of i*Z). + + The mapping can also be given a name. In some contexts, stimflow requires that Pauli mappings + have a name (e.g. when specifying the Pauli mapping of a logical operator for a stabilizer code). + + Examples: + >>> import stimflow as sf + >>> p1 = sf.PauliMap({0: "X", 1: "Y", 2: "Z"}) + >>> p2 = sf.PauliMap.from_xs([1, 2, 3]) + >>> p3 = sf.PauliMap({"Z": [3, 4j]}) + >>> print(p1 * p2 * p3) + X0*Z4j*Z1*Y2*Y3 + """ def __init__( self, @@ -45,22 +60,47 @@ def __init__( name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, in order to identify the Pauli map. A common convention used in the library is that named Pauli maps correspond to logical operators. + + Examples: + >>> import stimflow as sf + >>> import stim + + >>> print(sf.PauliMap()) + I + + >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"})) + X0*Y1*Z2 + + >>> print(sf.PauliMap({"X": [1, 2], "Y": 1+1j})) + X1*Y(1+1j)*X2 + + >>> print(sf.PauliMap(stim.PauliString("XYZ_X"))) + X0*Y1*Z2*X4 + + >>> print(sf.PauliMap(sf.Tile(data_qubits=[1, 2, 3], bases="X"))) + X1*X2*X3 + + >>> print(sf.PauliMap({0: "X", "Y": [0, 1]})) + Z0*Y1 + + >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"}, name="test")) + (name='test') X0*Y1*Z2 """ - self.qubits: dict[complex, Literal["X", "Y", "Z"]] - self.name = name + self._dict: dict[complex, Literal["X", "Y", "Z"]] + self.name: Any = name self._hash: int from stimflow._core._tile import Tile if isinstance(mapping, Tile): - self.qubits = dict(mapping.to_pauli_map().qubits) + self._dict = dict(mapping.to_pauli_map().items()) elif isinstance(mapping, PauliMap): - self.qubits = dict(mapping.qubits) + self._dict = dict(mapping.items()) elif isinstance(mapping, stim.PauliString): - self.qubits = {q: cast(Any, "_XYZ"[mapping[q]]) for q in mapping.pauli_indices()} + self._dict = {q: cast(Any, "_XYZ"[mapping[q]]) for q in mapping.pauli_indices()} elif mapping is not None: - self.qubits = {} + self._dict = {} for k, v in mapping.items(): if (v == "X" or v == "Y" or v == "Z") and isinstance(k, (int, float, complex)): self._mul_term(k, cast(Any, v)) @@ -77,10 +117,10 @@ def __init__( else: raise ValueError(f"Don't know how to interpret {k=}: {v=} as a pauli mapping.") - self.qubits = {complex(q): self.qubits[q] for q in sorted_complex(self.keys())} + self._dict = {complex(q): self._dict[q] for q in sorted_complex(self.keys())} else: - self.qubits = {} - self._hash = hash((self.name, tuple(self.qubits.items()))) + self._dict = {} + self._hash = hash((self.name, tuple(self._dict.items()))) @staticmethod def from_xs(xs: Iterable[complex], *, name: Any = None) -> PauliMap: @@ -98,32 +138,32 @@ def from_zs(zs: Iterable[complex], *, name: Any = None) -> PauliMap: return PauliMap({"Z": zs}, name=name) def __contains__(self, item: complex) -> bool: - """Determines if the PauliMap contains maps the given qubit to a non-identity Pauli.""" - return self.qubits.__contains__(item) + """Determines if the PauliMap maps the given qubit to a non-identity Pauli.""" + return self._dict.__contains__(item) def items(self) -> Iterable[tuple[complex, Literal["X", "Y", "Z"]]]: """Returns the (qubit, basis) pairs of the PauliMap.""" - return self.qubits.items() + return self._dict.items() def values(self) -> Iterable[Literal["X", "Y", "Z"]]: """Returns the bases used by the PauliMap.""" - return self.qubits.values() + return self._dict.values() def keys(self) -> Set[complex]: """Returns the qubits of the PauliMap.""" - return self.qubits.keys() + return self._dict.keys() def get(self, key: complex, default: Any = None) -> Any: - return self.qubits.get(key, default) + return self._dict.get(key, default) def __getitem__(self, item: complex) -> Literal["I", "X", "Y", "Z"]: - return cast(Any, self.qubits.get(item, "I")) + return cast(Any, self._dict.get(item, "I")) def __len__(self) -> int: - return len(self.qubits) + return len(self._dict) def __iter__(self) -> Iterator[complex]: - return self.qubits.__iter__() + return self._dict.__iter__() def with_name(self, name: Any) -> PauliMap: """Returns the same PauliMap, but with the given name. @@ -133,16 +173,16 @@ def with_name(self, name: Any) -> PauliMap: return PauliMap(self, name=name) def _mul_term(self, q: complex, b: Literal["X", "Y", "Z"]): - new_b = _multiplication_table[self.qubits.pop(q, None)][b] + new_b = _multiplication_table[self._dict.pop(q, None)][b] if new_b is not None: - self.qubits[q] = new_b + self._dict[q] = new_b def with_basis(self, basis: Literal["X", "Y", "Z"]) -> PauliMap: """Returns the same PauliMap, but with all its qubits mapped to the given basis.""" return PauliMap({q: basis for q in self.keys()}, name=self.name) def __bool__(self) -> bool: - return bool(self.qubits) + return bool(self._dict) def __mul__(self, other: PauliMap | Tile) -> PauliMap: from stimflow._core._tile import Tile @@ -152,8 +192,8 @@ def __mul__(self, other: PauliMap | Tile) -> PauliMap: result: dict[complex, Literal["X", "Y", "Z"]] = {} for q in self.keys() | other.keys(): - a = self.qubits.get(q, "I") - b = other.qubits.get(q, "I") + a = self._dict.get(q, "I") + b = other._dict.get(q, "I") ax = a in "XY" az = a in "YZ" bx = b in "XY" @@ -170,14 +210,14 @@ def __repr__(self) -> str: s2 = "" else: s2 = f", name={self.name!r}" - qs = sorted_complex(self.qubits) + qs = sorted_complex(self._dict) if len(self) > 1: p = set(self.values()) if p == {'X'}: return f"stimflow.PauliMap.from_xs({qs!r}{s2})" if p == {'Z'}: return f"stimflow.PauliMap.from_zs({qs!r}{s2})" - s = {q: self.qubits[q] for q in qs} + s = {q: self._dict[q] for q in qs} return f"stimflow.PauliMap({s!r}{s2})" def __str__(self) -> str: @@ -188,7 +228,9 @@ def simplify(c: complex) -> str: return str(c.real) return str(c) - result = "*".join(f"{self.qubits[q]}{simplify(q)}" for q in sorted_complex(self.keys())) + result = "*".join(f"{self._dict[q]}{simplify(q)}" for q in sorted_complex(self.keys())) + if not result: + result = 'I' if self.name is not None: result = f"(name={self.name!r}) " + result return result @@ -196,12 +238,12 @@ def simplify(c: complex) -> str: def with_xz_flipped(self) -> PauliMap: """Returns the same PauliMap, but with all qubits conjugated by H.""" remap = {"X": "Z", "Y": "Y", "Z": "X"} - return PauliMap({q: remap[p] for q, p in self.qubits.items()}, name=self.name) + return PauliMap({q: remap[p] for q, p in self._dict.items()}, name=self.name) def with_xy_flipped(self) -> PauliMap: """Returns the same PauliMap, but with all qubits conjugated by H_XY.""" remap = {"X": "Y", "Y": "X", "Z": "Z"} - return PauliMap({q: remap[p] for q, p in self.qubits.items()}, name=self.name) + return PauliMap({q: remap[p] for q, p in self._dict.items()}, name=self.name) def commutes(self, other: PauliMap) -> bool: """Determines if the pauli map commutes with another pauli map.""" @@ -211,12 +253,12 @@ def anticommutes(self, other: PauliMap) -> bool: """Determines if the pauli map anticommutes with another pauli map.""" t = 0 for q in self.keys() & other.keys(): - t += self.qubits[q] != other.qubits[q] + t += self._dict[q] != other._dict[q] return t % 2 == 1 def with_transformed_coords(self, transform: Callable[[complex], complex]) -> PauliMap: """Returns the same PauliMap but with coordinates transformed by the given function.""" - return PauliMap({transform(q): p for q, p in self.qubits.items()}, name=self.name) + return PauliMap({transform(q): p for q, p in self._dict.items()}, name=self.name) def to_stim_pauli_string( self, q2i: dict[complex, int], *, num_qubits: int | None = None @@ -253,10 +295,10 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, PauliMap): return NotImplemented - return self.qubits == other.qubits + return self._dict == other._dict def _sort_key(self) -> Any: - return tuple((q.real, q.imag, p) for q, p in self.qubits.items()) + return tuple((q.real, q.imag, p) for q, p in self._dict.items()) def __lt__(self, other) -> bool: if not isinstance(other, PauliMap): From da3aeb15b53d11062f71858cb83b6612851cc15f Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 15:21:16 -0700 Subject: [PATCH 4/9] regen doc --- glue/stimflow/doc/api.md | 89 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 8addcd11..27df0dd3 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -290,7 +290,26 @@ import numpy as np # (at top-level in the stimflow module) class Chunk: - """A circuit chunk with accompanying stabilizer flow assertions. + """A circuit with accompanying stabilizer flow assertions. + + This object is intended to be immutable. + Some of its fields are editable types, but it is assumed they do not change + (e.g. computations may be cached). + Don't do things like appending to the circuit of a chunk after the chunk is created. + + Example: + >>> import stimflow as sf + >>> import stim + >>> chunk = sf.Chunk( + ... circuit=stim.Circuit(''' + ... QUBIT_COORDS(1, 2) 0 + ... H 0 + ... '''), + ... flows=[ + ... sf.Flow(start=sf.PauliMap({1+2j: "X"}), end=sf.PauliMap({1+2j: "Z"})), + ... ], + ... ) + >>> chunk.verify() """ ``` @@ -311,7 +330,7 @@ def __init__( q2i: dict[complex, int] | None = None, o2i: dict[Any, int] | None = None, ): - """ + """Creates a `stimflow.Chunk` with the given values. Args: circuit: The circuit implementing the chunk's functionality. @@ -330,16 +349,32 @@ def __init__( flowing in. wants_to_merge_with_next: Defaults to False. When set to True, the chunk compiler won't insert a TICK between this chunk - and the next chunk. + and the next chunk. For example, this is useful when creating a + transversal initialization chunk. wants_to_merge_with_prev: Defaults to False. When set to True, the chunk compiler won't insert a TICK between this chunk - and the previous chunk. + and the previous chunk. For example, this is useful when creating a + transversal measurement chunk. q2i: Defaults to None (infer from QUBIT_COORDS instructions in circuit else raise an exception). The stimflow-qubit-coordinate-to-stim-qubit-index mapping used to translate between stimflow's qubit keys and stim's qubit keys. o2i: Defaults to None (raise an exception if observables present in circuit). The stimflow-observable-key-to-stim-observable-index mapping used to translate between stimflow's observable keys and stim's observable keys. + + Example: + >>> import stimflow as sf + >>> import stim + >>> chunk = sf.Chunk( + ... circuit=stim.Circuit(''' + ... QUBIT_COORDS(1, 2) 0 + ... H 0 + ... '''), + ... flows=[ + ... sf.Flow(start=sf.PauliMap({1+2j: "X"}), end=sf.PauliMap({1+2j: "Z"})), + ... ], + ... ) + >>> chunk.verify() """ ``` @@ -552,7 +587,42 @@ def verify_distance_is_at_least( ): """Verifies undetected logical errors require at least the given number of physical errors. - By default, verifies using a uniform depolarizing circuit noise model. + Args: + minimum_distance: The minimum distance to verify. Currently this must be at most 3. + noise: The noise model to use. Defaults to a uniform depolarizing circuit noise model + that allows multiple operations per tick and where two qubit gates apply two qubit + depolarizing noise. + + Example: + >>> import stimflow as sf + >>> import stim + >>> lz = sf.PauliMap({0: "Z"}).with_name("LZ") + >>> zz01 = sf.PauliMap.from_zs([0, 1]) + >>> zz12 = sf.PauliMap.from_zs([1, 2]) + >>> zz23 = sf.PauliMap.from_zs([2, 3]) + >>> zz34 = sf.PauliMap.from_zs([3, 4]) + >>> chunk = sf.Chunk( + ... stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(1, 0) 1 + ... QUBIT_COORDS(2, 0) 2 + ... QUBIT_COORDS(3, 0) 3 + ... QUBIT_COORDS(4, 0) 4 + ... MZZ 0 1 1 2 2 3 3 4 + ... '''), + ... flows=[ + ... sf.Flow(start=lz, end=lz), + ... sf.Flow(start=zz01, mids=[0]), + ... sf.Flow(start=zz12, mids=[1]), + ... sf.Flow(start=zz23, mids=[2]), + ... sf.Flow(start=zz34, mids=[3]), + ... sf.Flow(end=zz01, mids=[0]), + ... sf.Flow(end=zz12, mids=[1]), + ... sf.Flow(end=zz23, mids=[2]), + ... sf.Flow(end=zz34, mids=[3]), + ... ], + ... ) + >>> chunk.verify_distance_is_at_least(3) """ ``` @@ -2364,6 +2434,7 @@ def uniform_depolarizing( p: float, *, single_qubit_only: bool = False, + allow_multiple_uses_of_a_qubit_in_one_tick: bool = False, ) -> NoiseModel: """Near-standard circuit depolarizing noise. @@ -2375,6 +2446,14 @@ def uniform_depolarizing( Non-demolition measurement is treated a bit unusually in that it is the result that is flipped instead of the input qubit. The input qubit is depolarized. + + Args: + single_qubit_only: Defaults to False. When False, two qubit gates apply two + qubit depolarizing noise (DEPOLARIZE2). When True, they instead apply single qubit + depolarizing noise (DEPOLARIZE1). + allow_multiple_uses_of_a_qubit_in_one_tick: Defaults to False. When False, an error will be + raised if attempting to add noise to a circuit that operates on a qubit + multiple times between TICK operations. When set to True, no error is raised. """ ``` From 642cfbf9d8c20371b32476c7dc73f21d12573ced Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 15:34:25 -0700 Subject: [PATCH 5/9] prefix with layer instead of suffix with layer --- glue/stimflow/doc/api.md | 664 +++++++++--------- glue/stimflow/src/stimflow/__init__.py | 8 +- .../stimflow/src/stimflow/_layers/__init__.py | 8 +- glue/stimflow/src/stimflow/_layers/_layer.py | 6 +- .../src/stimflow/_layers/_layer_circuit.py | 164 ++--- .../stimflow/_layers/_layer_circuit_test.py | 8 +- ..._layer.py => _layer_det_obs_annotation.py} | 0 .../{_empty_layer.py => _layer_empty.py} | 6 +- ...{_feedback_layer.py => _layer_feedback.py} | 14 +- ..._layer_test.py => _layer_feedback_test.py} | 2 +- ...{_interact_layer.py => _layer_interact.py} | 24 +- ..._swap_layer.py => _layer_interact_swap.py} | 22 +- ...r_test.py => _layer_interact_swap_test.py} | 26 +- .../{_iswap_layer.py => _layer_iswap.py} | 6 +- .../{_loop_layer.py => _layer_loop.py} | 10 +- .../{_measure_layer.py => _layer_measure.py} | 18 +- .../_layers/{_mpp_layer.py => _layer_mpp.py} | 6 +- .../{_noise_layer.py => _layer_noise.py} | 6 +- ...er.py => _layer_qubit_coord_annotation.py} | 4 +- .../{_reset_layer.py => _layer_reset.py} | 16 +- ...{_rotation_layer.py => _layer_rotation.py} | 26 +- ..._layer_test.py => _layer_rotation_test.py} | 4 +- ...er.py => _layer_shift_coord_annotation.py} | 10 +- .../{_sqrt_pp_layer.py => _layer_sqrt_pp.py} | 14 +- .../{_swap_layer.py => _layer_swap.py} | 26 +- .../_layers/{_tag_layer.py => _layer_tag.py} | 6 +- ...{_tag_layer_test.py => _layer_tag_test.py} | 0 27 files changed, 552 insertions(+), 552 deletions(-) rename glue/stimflow/src/stimflow/_layers/{_det_obs_annotation_layer.py => _layer_det_obs_annotation.py} (100%) rename glue/stimflow/src/stimflow/_layers/{_empty_layer.py => _layer_empty.py} (83%) rename glue/stimflow/src/stimflow/_layers/{_feedback_layer.py => _layer_feedback.py} (83%) rename glue/stimflow/src/stimflow/_layers/{_feedback_layer_test.py => _layer_feedback_test.py} (76%) rename glue/stimflow/src/stimflow/_layers/{_interact_layer.py => _layer_interact.py} (83%) rename glue/stimflow/src/stimflow/_layers/{_interact_swap_layer.py => _layer_interact_swap.py} (80%) rename glue/stimflow/src/stimflow/_layers/{_interact_swap_layer_test.py => _layer_interact_swap_test.py} (66%) rename glue/stimflow/src/stimflow/_layers/{_iswap_layer.py => _layer_iswap.py} (87%) rename glue/stimflow/src/stimflow/_layers/{_loop_layer.py => _layer_loop.py} (81%) rename glue/stimflow/src/stimflow/_layers/{_measure_layer.py => _layer_measure.py} (75%) rename glue/stimflow/src/stimflow/_layers/{_mpp_layer.py => _layer_mpp.py} (85%) rename glue/stimflow/src/stimflow/_layers/{_noise_layer.py => _layer_noise.py} (85%) rename glue/stimflow/src/stimflow/_layers/{_qubit_coord_annotation_layer.py => _layer_qubit_coord_annotation.py} (89%) rename glue/stimflow/src/stimflow/_layers/{_reset_layer.py => _layer_reset.py} (78%) rename glue/stimflow/src/stimflow/_layers/{_rotation_layer.py => _layer_rotation.py} (77%) rename glue/stimflow/src/stimflow/_layers/{_rotation_layer_test.py => _layer_rotation_test.py} (90%) rename glue/stimflow/src/stimflow/_layers/{_shift_coord_annotation_layer.py => _layer_shift_coord_annotation.py} (77%) rename glue/stimflow/src/stimflow/_layers/{_sqrt_pp_layer.py => _layer_sqrt_pp.py} (85%) rename glue/stimflow/src/stimflow/_layers/{_swap_layer.py => _layer_swap.py} (77%) rename glue/stimflow/src/stimflow/_layers/{_tag_layer.py => _layer_tag.py} (89%) rename glue/stimflow/src/stimflow/_layers/{_tag_layer_test.py => _layer_tag_test.py} (100%) diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 27df0dd3..14a2acea 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -99,13 +99,6 @@ - [`stimflow.Flow.with_xz_flipped`](#stimflow.Flow.with_xz_flipped) - [`stimflow.FlowMetadata`](#stimflow.FlowMetadata) - [`stimflow.FlowMetadata.__init__`](#stimflow.FlowMetadata.__init__) -- [`stimflow.InteractLayer`](#stimflow.InteractLayer) - - [`stimflow.InteractLayer.append_into_stim_circuit`](#stimflow.InteractLayer.append_into_stim_circuit) - - [`stimflow.InteractLayer.copy`](#stimflow.InteractLayer.copy) - - [`stimflow.InteractLayer.locally_optimized`](#stimflow.InteractLayer.locally_optimized) - - [`stimflow.InteractLayer.rotate_to_z_layer`](#stimflow.InteractLayer.rotate_to_z_layer) - - [`stimflow.InteractLayer.to_z_basis`](#stimflow.InteractLayer.to_z_basis) - - [`stimflow.InteractLayer.touched`](#stimflow.InteractLayer.touched) - [`stimflow.LayerCircuit`](#stimflow.LayerCircuit) - [`stimflow.LayerCircuit.copy`](#stimflow.LayerCircuit.copy) - [`stimflow.LayerCircuit.from_stim_circuit`](#stimflow.LayerCircuit.from_stim_circuit) @@ -126,15 +119,37 @@ - [`stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type`](#stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type) - [`stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier`](#stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier) - [`stimflow.LayerCircuit.without_empty_layers`](#stimflow.LayerCircuit.without_empty_layers) +- [`stimflow.LayerInteract`](#stimflow.LayerInteract) + - [`stimflow.LayerInteract.append_into_stim_circuit`](#stimflow.LayerInteract.append_into_stim_circuit) + - [`stimflow.LayerInteract.copy`](#stimflow.LayerInteract.copy) + - [`stimflow.LayerInteract.locally_optimized`](#stimflow.LayerInteract.locally_optimized) + - [`stimflow.LayerInteract.rotate_to_z_layer`](#stimflow.LayerInteract.rotate_to_z_layer) + - [`stimflow.LayerInteract.to_z_basis`](#stimflow.LayerInteract.to_z_basis) + - [`stimflow.LayerInteract.touched`](#stimflow.LayerInteract.touched) +- [`stimflow.LayerMeasure`](#stimflow.LayerMeasure) + - [`stimflow.LayerMeasure.append_into_stim_circuit`](#stimflow.LayerMeasure.append_into_stim_circuit) + - [`stimflow.LayerMeasure.copy`](#stimflow.LayerMeasure.copy) + - [`stimflow.LayerMeasure.locally_optimized`](#stimflow.LayerMeasure.locally_optimized) + - [`stimflow.LayerMeasure.to_z_basis`](#stimflow.LayerMeasure.to_z_basis) + - [`stimflow.LayerMeasure.touched`](#stimflow.LayerMeasure.touched) +- [`stimflow.LayerReset`](#stimflow.LayerReset) + - [`stimflow.LayerReset.append_into_stim_circuit`](#stimflow.LayerReset.append_into_stim_circuit) + - [`stimflow.LayerReset.copy`](#stimflow.LayerReset.copy) + - [`stimflow.LayerReset.locally_optimized`](#stimflow.LayerReset.locally_optimized) + - [`stimflow.LayerReset.to_z_basis`](#stimflow.LayerReset.to_z_basis) + - [`stimflow.LayerReset.touched`](#stimflow.LayerReset.touched) +- [`stimflow.LayerRotation`](#stimflow.LayerRotation) + - [`stimflow.LayerRotation.append_into_stim_circuit`](#stimflow.LayerRotation.append_into_stim_circuit) + - [`stimflow.LayerRotation.append_named_rotation`](#stimflow.LayerRotation.append_named_rotation) + - [`stimflow.LayerRotation.copy`](#stimflow.LayerRotation.copy) + - [`stimflow.LayerRotation.inverse`](#stimflow.LayerRotation.inverse) + - [`stimflow.LayerRotation.is_vacuous`](#stimflow.LayerRotation.is_vacuous) + - [`stimflow.LayerRotation.locally_optimized`](#stimflow.LayerRotation.locally_optimized) + - [`stimflow.LayerRotation.prepend_named_rotation`](#stimflow.LayerRotation.prepend_named_rotation) + - [`stimflow.LayerRotation.touched`](#stimflow.LayerRotation.touched) - [`stimflow.LineDataFor3DModel`](#stimflow.LineDataFor3DModel) - [`stimflow.LineDataFor3DModel.__init__`](#stimflow.LineDataFor3DModel.__init__) - [`stimflow.LineDataFor3DModel.fused`](#stimflow.LineDataFor3DModel.fused) -- [`stimflow.MeasureLayer`](#stimflow.MeasureLayer) - - [`stimflow.MeasureLayer.append_into_stim_circuit`](#stimflow.MeasureLayer.append_into_stim_circuit) - - [`stimflow.MeasureLayer.copy`](#stimflow.MeasureLayer.copy) - - [`stimflow.MeasureLayer.locally_optimized`](#stimflow.MeasureLayer.locally_optimized) - - [`stimflow.MeasureLayer.to_z_basis`](#stimflow.MeasureLayer.to_z_basis) - - [`stimflow.MeasureLayer.touched`](#stimflow.MeasureLayer.touched) - [`stimflow.NoiseModel`](#stimflow.NoiseModel) - [`stimflow.NoiseModel.noisy_circuit`](#stimflow.NoiseModel.noisy_circuit) - [`stimflow.NoiseModel.noisy_circuit_skipping_mpp_boundaries`](#stimflow.NoiseModel.noisy_circuit_skipping_mpp_boundaries) @@ -177,21 +192,6 @@ - [`stimflow.PauliMap.with_transformed_coords`](#stimflow.PauliMap.with_transformed_coords) - [`stimflow.PauliMap.with_xy_flipped`](#stimflow.PauliMap.with_xy_flipped) - [`stimflow.PauliMap.with_xz_flipped`](#stimflow.PauliMap.with_xz_flipped) -- [`stimflow.ResetLayer`](#stimflow.ResetLayer) - - [`stimflow.ResetLayer.append_into_stim_circuit`](#stimflow.ResetLayer.append_into_stim_circuit) - - [`stimflow.ResetLayer.copy`](#stimflow.ResetLayer.copy) - - [`stimflow.ResetLayer.locally_optimized`](#stimflow.ResetLayer.locally_optimized) - - [`stimflow.ResetLayer.to_z_basis`](#stimflow.ResetLayer.to_z_basis) - - [`stimflow.ResetLayer.touched`](#stimflow.ResetLayer.touched) -- [`stimflow.RotationLayer`](#stimflow.RotationLayer) - - [`stimflow.RotationLayer.append_into_stim_circuit`](#stimflow.RotationLayer.append_into_stim_circuit) - - [`stimflow.RotationLayer.append_named_rotation`](#stimflow.RotationLayer.append_named_rotation) - - [`stimflow.RotationLayer.copy`](#stimflow.RotationLayer.copy) - - [`stimflow.RotationLayer.inverse`](#stimflow.RotationLayer.inverse) - - [`stimflow.RotationLayer.is_vacuous`](#stimflow.RotationLayer.is_vacuous) - - [`stimflow.RotationLayer.locally_optimized`](#stimflow.RotationLayer.locally_optimized) - - [`stimflow.RotationLayer.prepend_named_rotation`](#stimflow.RotationLayer.prepend_named_rotation) - - [`stimflow.RotationLayer.touched`](#stimflow.RotationLayer.touched) - [`stimflow.StabilizerCode`](#stimflow.StabilizerCode) - [`stimflow.StabilizerCode.__init__`](#stimflow.StabilizerCode.__init__) - [`stimflow.StabilizerCode.as_interface`](#stimflow.StabilizerCode.as_interface) @@ -1811,83 +1811,6 @@ def __init__( """ ``` - -```python -# stimflow.InteractLayer - -# (at top-level in the stimflow module) -@dataclasses.dataclass -class InteractLayer: - """A layer of controlled Pauli gates (like CX, CZ, and XCY). - """ - targets1: list[int] - targets2: list[int] - bases1: list[str] - bases2: list[str] -``` - - -```python -# stimflow.InteractLayer.append_into_stim_circuit - -# (in class stimflow.InteractLayer) -def append_into_stim_circuit( - self, - out: stim.Circuit, -) -> None: -``` - - -```python -# stimflow.InteractLayer.copy - -# (in class stimflow.InteractLayer) -def copy( - self, -) -> InteractLayer: -``` - - -```python -# stimflow.InteractLayer.locally_optimized - -# (in class stimflow.InteractLayer) -def locally_optimized( - self, - next_layer: Layer | None, -) -> list[Layer | None]: -``` - - -```python -# stimflow.InteractLayer.rotate_to_z_layer - -# (in class stimflow.InteractLayer) -def rotate_to_z_layer( - self, -): -``` - - -```python -# stimflow.InteractLayer.to_z_basis - -# (in class stimflow.InteractLayer) -def to_z_basis( - self, -) -> list[Layer]: -``` - - -```python -# stimflow.InteractLayer.touched - -# (in class stimflow.InteractLayer) -def touched( - self, -) -> set[int]: -``` - ```python # stimflow.LayerCircuit @@ -1897,8 +1820,8 @@ def touched( class LayerCircuit: """A stabilizer circuit represented as a series of typed layers. - For example, the circuit could be a `ResetLayer`, then a `RotationLayer`, - then a few `InteractLayer`s, then a `MeasureLayer`. + For example, the circuit could be a `LayerReset`, then a `LayerRotation`, + then a few `LayerInteract`s, then a `LayerMeasure`. """ layers: list[Layer] ``` @@ -2221,132 +2144,371 @@ def without_empty_layers( """ ``` - + ```python -# stimflow.LineDataFor3DModel +# stimflow.LayerInteract # (at top-level in the stimflow module) -class LineDataFor3DModel: - """Coordinates and colors of lines to draw in a 3d model. - - Example: - >>> import stimflow as sf - >>> red_square_outline = sf.LineDataFor3DModel( - ... rgba=(1, 0, 0, 1), - ... edge_list=[ - ... # A square made of four lines. - ... [(0, 0, 0), (0, 1, 0)], - ... [(0, 1, 0), (1, 1, 0)], - ... [(1, 1, 0), (1, 0, 0)], - ... [(1, 0, 0), (0, 0, 0)], - ... ], - ... ) - >>> model = sf.make_3d_model([red_square_outline]) - >>> assert model.html_viewer() is not None +@dataclasses.dataclass +class LayerInteract: + """A layer of controlled Pauli gates (like CX, CZ, and XCY). """ + targets1: list[int] + targets2: list[int] + bases1: list[str] + bases2: list[str] ``` - + ```python -# stimflow.LineDataFor3DModel.__init__ +# stimflow.LayerInteract.append_into_stim_circuit -# (in class stimflow.LineDataFor3DModel) -def __init__( +# (in class stimflow.LayerInteract) +def append_into_stim_circuit( + self, + out: stim.Circuit, +) -> None: +``` + + +```python +# stimflow.LayerInteract.copy + +# (in class stimflow.LayerInteract) +def copy( + self, +) -> LayerInteract: +``` + + +```python +# stimflow.LayerInteract.locally_optimized + +# (in class stimflow.LayerInteract) +def locally_optimized( + self, + next_layer: Layer | None, +) -> list[Layer | None]: +``` + + +```python +# stimflow.LayerInteract.rotate_to_z_layer + +# (in class stimflow.LayerInteract) +def rotate_to_z_layer( self, - *, - rgba: tuple[float, float, float, float], - edge_list: np.ndarray | Iterable[Sequence[Sequence[float]]], ): - """Lines with associated color information. +``` - Args: - rgba: Red, green, blue, and alpha data to associate with all the lines. - Each value should range from 0 to 1. - (The alpha data is ignored in most viewers, but needed by the 3d model format.) - edge_list: A 3d float32 numpy array with shape == (*, 2, 3). - Axis 0 is the triangle axis (each entry is a triangle). - Axis 1 is the AB vertex axis (each entry is a vertex from the edge). - Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). - """ + +```python +# stimflow.LayerInteract.to_z_basis + +# (in class stimflow.LayerInteract) +def to_z_basis( + self, +) -> list[Layer]: ``` - + ```python -# stimflow.LineDataFor3DModel.fused +# stimflow.LayerInteract.touched -# (in class stimflow.LineDataFor3DModel) -def fused( - data: Iterable[LineDataFor3DModel], -) -> list[LineDataFor3DModel]: - """Attempts to combine line data instances into fewer instances. - """ +# (in class stimflow.LayerInteract) +def touched( + self, +) -> set[int]: ``` - + ```python -# stimflow.MeasureLayer +# stimflow.LayerMeasure # (at top-level in the stimflow module) @dataclasses.dataclass -class MeasureLayer: +class LayerMeasure: """A layer of single qubit Pauli basis measurement operations. """ targets: list[int] bases: list[str] ``` - + +```python +# stimflow.LayerMeasure.append_into_stim_circuit + +# (in class stimflow.LayerMeasure) +def append_into_stim_circuit( + self, + out: stim.Circuit, +) -> None: +``` + + +```python +# stimflow.LayerMeasure.copy + +# (in class stimflow.LayerMeasure) +def copy( + self, +) -> LayerMeasure: +``` + + +```python +# stimflow.LayerMeasure.locally_optimized + +# (in class stimflow.LayerMeasure) +def locally_optimized( + self, + next_layer: Layer | None, +) -> list[Layer | None]: +``` + + +```python +# stimflow.LayerMeasure.to_z_basis + +# (in class stimflow.LayerMeasure) +def to_z_basis( + self, +) -> list[Layer]: +``` + + +```python +# stimflow.LayerMeasure.touched + +# (in class stimflow.LayerMeasure) +def touched( + self, +) -> set[int]: +``` + + ```python -# stimflow.MeasureLayer.append_into_stim_circuit +# stimflow.LayerReset -# (in class stimflow.MeasureLayer) +# (at top-level in the stimflow module) +@dataclasses.dataclass +class LayerReset: + """A layer of reset gates. + """ + targets: dict[int, Literal['X', 'Y', 'Z']] +``` + + +```python +# stimflow.LayerReset.append_into_stim_circuit + +# (in class stimflow.LayerReset) def append_into_stim_circuit( self, out: stim.Circuit, ) -> None: ``` - + ```python -# stimflow.MeasureLayer.copy +# stimflow.LayerReset.copy -# (in class stimflow.MeasureLayer) +# (in class stimflow.LayerReset) def copy( self, -) -> MeasureLayer: +) -> LayerReset: ``` - + ```python -# stimflow.MeasureLayer.locally_optimized +# stimflow.LayerReset.locally_optimized -# (in class stimflow.MeasureLayer) +# (in class stimflow.LayerReset) def locally_optimized( self, next_layer: Layer | None, ) -> list[Layer | None]: ``` - + ```python -# stimflow.MeasureLayer.to_z_basis +# stimflow.LayerReset.to_z_basis -# (in class stimflow.MeasureLayer) +# (in class stimflow.LayerReset) def to_z_basis( self, ) -> list[Layer]: ``` - + +```python +# stimflow.LayerReset.touched + +# (in class stimflow.LayerReset) +def touched( + self, +) -> set[int]: +``` + + +```python +# stimflow.LayerRotation + +# (at top-level in the stimflow module) +@dataclasses.dataclass +class LayerRotation: + """A layer of single qubit Clifford rotation gates. + """ + named_rotations: dict[int, str] +``` + + +```python +# stimflow.LayerRotation.append_into_stim_circuit + +# (in class stimflow.LayerRotation) +def append_into_stim_circuit( + self, + out: stim.Circuit, +) -> None: +``` + + +```python +# stimflow.LayerRotation.append_named_rotation + +# (in class stimflow.LayerRotation) +def append_named_rotation( + self, + name: str, + target: int, +): +``` + + +```python +# stimflow.LayerRotation.copy + +# (in class stimflow.LayerRotation) +def copy( + self, +) -> LayerRotation: +``` + + +```python +# stimflow.LayerRotation.inverse + +# (in class stimflow.LayerRotation) +def inverse( + self, +) -> LayerRotation: +``` + + +```python +# stimflow.LayerRotation.is_vacuous + +# (in class stimflow.LayerRotation) +def is_vacuous( + self, +) -> bool: +``` + + +```python +# stimflow.LayerRotation.locally_optimized + +# (in class stimflow.LayerRotation) +def locally_optimized( + self, + next_layer: Layer | None, +) -> list[Layer | None]: +``` + + +```python +# stimflow.LayerRotation.prepend_named_rotation + +# (in class stimflow.LayerRotation) +def prepend_named_rotation( + self, + name: str, + target: int, +): +``` + + ```python -# stimflow.MeasureLayer.touched +# stimflow.LayerRotation.touched -# (in class stimflow.MeasureLayer) +# (in class stimflow.LayerRotation) def touched( self, ) -> set[int]: ``` + +```python +# stimflow.LineDataFor3DModel + +# (at top-level in the stimflow module) +class LineDataFor3DModel: + """Coordinates and colors of lines to draw in a 3d model. + + Example: + >>> import stimflow as sf + >>> red_square_outline = sf.LineDataFor3DModel( + ... rgba=(1, 0, 0, 1), + ... edge_list=[ + ... # A square made of four lines. + ... [(0, 0, 0), (0, 1, 0)], + ... [(0, 1, 0), (1, 1, 0)], + ... [(1, 1, 0), (1, 0, 0)], + ... [(1, 0, 0), (0, 0, 0)], + ... ], + ... ) + >>> model = sf.make_3d_model([red_square_outline]) + >>> assert model.html_viewer() is not None + """ +``` + + +```python +# stimflow.LineDataFor3DModel.__init__ + +# (in class stimflow.LineDataFor3DModel) +def __init__( + self, + *, + rgba: tuple[float, float, float, float], + edge_list: np.ndarray | Iterable[Sequence[Sequence[float]]], +): + """Lines with associated color information. + + Args: + rgba: Red, green, blue, and alpha data to associate with all the lines. + Each value should range from 0 to 1. + (The alpha data is ignored in most viewers, but needed by the 3d model format.) + edge_list: A 3d float32 numpy array with shape == (*, 2, 3). + Axis 0 is the triangle axis (each entry is a triangle). + Axis 1 is the AB vertex axis (each entry is a vertex from the edge). + Axis 2 is the XYZ coordinate axis (each entry is a coordinate from the vertex). + """ +``` + + +```python +# stimflow.LineDataFor3DModel.fused + +# (in class stimflow.LineDataFor3DModel) +def fused( + data: Iterable[LineDataFor3DModel], +) -> list[LineDataFor3DModel]: + """Attempts to combine line data instances into fewer instances. + """ +``` + ```python # stimflow.NoiseModel @@ -2966,168 +3128,6 @@ def with_xz_flipped( """ ``` - -```python -# stimflow.ResetLayer - -# (at top-level in the stimflow module) -@dataclasses.dataclass -class ResetLayer: - """A layer of reset gates. - """ - targets: dict[int, Literal['X', 'Y', 'Z']] -``` - - -```python -# stimflow.ResetLayer.append_into_stim_circuit - -# (in class stimflow.ResetLayer) -def append_into_stim_circuit( - self, - out: stim.Circuit, -) -> None: -``` - - -```python -# stimflow.ResetLayer.copy - -# (in class stimflow.ResetLayer) -def copy( - self, -) -> ResetLayer: -``` - - -```python -# stimflow.ResetLayer.locally_optimized - -# (in class stimflow.ResetLayer) -def locally_optimized( - self, - next_layer: Layer | None, -) -> list[Layer | None]: -``` - - -```python -# stimflow.ResetLayer.to_z_basis - -# (in class stimflow.ResetLayer) -def to_z_basis( - self, -) -> list[Layer]: -``` - - -```python -# stimflow.ResetLayer.touched - -# (in class stimflow.ResetLayer) -def touched( - self, -) -> set[int]: -``` - - -```python -# stimflow.RotationLayer - -# (at top-level in the stimflow module) -@dataclasses.dataclass -class RotationLayer: - """A layer of single qubit Clifford rotation gates. - """ - named_rotations: dict[int, str] -``` - - -```python -# stimflow.RotationLayer.append_into_stim_circuit - -# (in class stimflow.RotationLayer) -def append_into_stim_circuit( - self, - out: stim.Circuit, -) -> None: -``` - - -```python -# stimflow.RotationLayer.append_named_rotation - -# (in class stimflow.RotationLayer) -def append_named_rotation( - self, - name: str, - target: int, -): -``` - - -```python -# stimflow.RotationLayer.copy - -# (in class stimflow.RotationLayer) -def copy( - self, -) -> RotationLayer: -``` - - -```python -# stimflow.RotationLayer.inverse - -# (in class stimflow.RotationLayer) -def inverse( - self, -) -> RotationLayer: -``` - - -```python -# stimflow.RotationLayer.is_vacuous - -# (in class stimflow.RotationLayer) -def is_vacuous( - self, -) -> bool: -``` - - -```python -# stimflow.RotationLayer.locally_optimized - -# (in class stimflow.RotationLayer) -def locally_optimized( - self, - next_layer: Layer | None, -) -> list[Layer | None]: -``` - - -```python -# stimflow.RotationLayer.prepend_named_rotation - -# (in class stimflow.RotationLayer) -def prepend_named_rotation( - self, - name: str, - target: int, -): -``` - - -```python -# stimflow.RotationLayer.touched - -# (in class stimflow.RotationLayer) -def touched( - self, -) -> set[int]: -``` - ```python # stimflow.StabilizerCode diff --git a/glue/stimflow/src/stimflow/__init__.py b/glue/stimflow/src/stimflow/__init__.py index 0051b073..39e533a4 100644 --- a/glue/stimflow/src/stimflow/__init__.py +++ b/glue/stimflow/src/stimflow/__init__.py @@ -39,11 +39,11 @@ xor_sorted, ) from stimflow._layers import ( - InteractLayer, + LayerInteract, LayerCircuit, - MeasureLayer, - ResetLayer, - RotationLayer, + LayerMeasure, + LayerReset, + LayerRotation, transpile_to_z_basis_interaction_circuit, ) from stimflow._viz import ( diff --git a/glue/stimflow/src/stimflow/_layers/__init__.py b/glue/stimflow/src/stimflow/_layers/__init__.py index 643c3f46..5bc482f7 100644 --- a/glue/stimflow/src/stimflow/_layers/__init__.py +++ b/glue/stimflow/src/stimflow/_layers/__init__.py @@ -1,8 +1,8 @@ """Works with circuits in a layered representation that's easy to operate on.""" -from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer_interact import LayerInteract from stimflow._layers._layer_circuit import LayerCircuit -from stimflow._layers._measure_layer import MeasureLayer -from stimflow._layers._reset_layer import ResetLayer -from stimflow._layers._rotation_layer import RotationLayer +from stimflow._layers._layer_measure import LayerMeasure +from stimflow._layers._layer_reset import LayerReset +from stimflow._layers._layer_rotation import LayerRotation from stimflow._layers._transpile import transpile_to_z_basis_interaction_circuit diff --git a/glue/stimflow/src/stimflow/_layers/_layer.py b/glue/stimflow/src/stimflow/_layers/_layer.py index 275186c0..04d338c8 100644 --- a/glue/stimflow/src/stimflow/_layers/_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer.py @@ -33,15 +33,15 @@ def append_into_stim_circuit(self, out: stim.Circuit) -> None: def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: """Returns an equivalent series of layers that has been optimized. - For example, if this is a RotationLayer and next_layer is also a RotationLayer, - then the result will be a single merged RotationLayer. + For example, if this is a LayerRotation and next_layer is also a LayerRotation, + then the result will be a single merged LayerRotation. """ return [self, next_layer] def is_vacuous(self) -> bool: """Returns True if the layer doesn't do anything. - For example, a RotationLayer with no rotations is vacuous. + For example, a LayerRotation with no rotations is vacuous. """ return False diff --git a/glue/stimflow/src/stimflow/_layers/_layer_circuit.py b/glue/stimflow/src/stimflow/_layers/_layer_circuit.py index 0df225e6..287ac185 100644 --- a/glue/stimflow/src/stimflow/_layers/_layer_circuit.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_circuit.py @@ -5,24 +5,24 @@ import stim -from stimflow._layers._det_obs_annotation_layer import DetObsAnnotationLayer -from stimflow._layers._empty_layer import EmptyLayer -from stimflow._layers._feedback_layer import FeedbackLayer -from stimflow._layers._interact_layer import InteractLayer -from stimflow._layers._interact_swap_layer import InteractSwapLayer -from stimflow._layers._iswap_layer import ISwapLayer +from stimflow._layers._layer_det_obs_annotation import DetObsAnnotationLayer +from stimflow._layers._layer_empty import LayerEmpty +from stimflow._layers._layer_feedback import LayerFeedback +from stimflow._layers._layer_interact import LayerInteract +from stimflow._layers._layer_interact_swap import LayerInteractSwap +from stimflow._layers._layer_iswap import LayerISwap from stimflow._layers._layer import Layer -from stimflow._layers._loop_layer import LoopLayer -from stimflow._layers._measure_layer import MeasureLayer -from stimflow._layers._mpp_layer import MppLayer -from stimflow._layers._noise_layer import NoiseLayer -from stimflow._layers._qubit_coord_annotation_layer import QubitCoordAnnotationLayer -from stimflow._layers._reset_layer import ResetLayer -from stimflow._layers._rotation_layer import RotationLayer -from stimflow._layers._shift_coord_annotation_layer import ShiftCoordAnnotationLayer -from stimflow._layers._sqrt_pp_layer import SqrtPPLayer -from stimflow._layers._swap_layer import SwapLayer -from stimflow._layers._tag_layer import TagLayer +from stimflow._layers._layer_loop import LayerLoop +from stimflow._layers._layer_measure import LayerMeasure +from stimflow._layers._layer_mpp import LayerMpp +from stimflow._layers._layer_noise import LayerNoise +from stimflow._layers._layer_qubit_coord_annotation import LayerQubitCoordAnnotation +from stimflow._layers._layer_reset import LayerReset +from stimflow._layers._layer_rotation import LayerRotation +from stimflow._layers._layer_shift_coord_annotation import LayerShiftCoordAnnotation +from stimflow._layers._layer_sqrt_pp import LayerSqrtPP +from stimflow._layers._layer_swap import LayerSwap +from stimflow._layers._layer_tag import LayerTag TLayer = TypeVar("TLayer") @@ -31,8 +31,8 @@ class LayerCircuit: """A stabilizer circuit represented as a series of typed layers. - For example, the circuit could be a `ResetLayer`, then a `RotationLayer`, - then a few `InteractLayer`s, then a `MeasureLayer`. + For example, the circuit could be a `LayerReset`, then a `LayerRotation`, + then a few `LayerInteract`s, then a `LayerMeasure`. """ layers: list[Layer] = dataclasses.field(default_factory=list) @@ -55,29 +55,29 @@ def to_z_basis(self) -> LayerCircuit: def _feed(self, kind: type[TLayer]) -> TLayer: if not self.layers: self.layers.append(cast(Layer, kind())) - elif isinstance(self.layers[-1], EmptyLayer): + elif isinstance(self.layers[-1], LayerEmpty): self.layers[-1] = cast(Layer, kind()) elif not isinstance(self.layers[-1], kind): self.layers.append(cast(Layer, kind())) return cast(TLayer, self.layers[-1]) def _feed_reset(self, basis: Literal["X", "Y", "Z"], targets: list[stim.GateTarget]): - layer = self._feed(ResetLayer) + layer = self._feed(LayerReset) for t in targets: layer.targets[t.value] = basis def _feed_tag(self, instruction: stim.CircuitInstruction): - layer = self._feed(TagLayer) + layer = self._feed(LayerTag) layer.circuit.append(instruction) def _feed_m(self, basis: Literal["X", "Y", "Z"], targets: list[stim.GateTarget]): - layer = self._feed(MeasureLayer) + layer = self._feed(LayerMeasure) for t in targets: layer.bases.append(basis) layer.targets.append(t.value) def _feed_mpp(self, targets: list[stim.GateTarget]): - layer = self._feed(MppLayer) + layer = self._feed(LayerMpp) start = 0 end = 1 while start < len(targets): @@ -88,7 +88,7 @@ def _feed_mpp(self, targets: list[stim.GateTarget]): end += 1 def _feed_qubit_coords(self, targets: list[stim.GateTarget], gate_args: list[float]): - layer = self._feed(QubitCoordAnnotationLayer) + layer = self._feed(LayerQubitCoordAnnotation) for target in targets: assert target.is_qubit_target q = target.value @@ -97,22 +97,22 @@ def _feed_qubit_coords(self, targets: list[stim.GateTarget], gate_args: list[flo layer.coords[q] = list(gate_args) def _feed_shift_coords(self, gate_args: list[float]): - self._feed(ShiftCoordAnnotationLayer).offset_by(gate_args) + self._feed(LayerShiftCoordAnnotation).offset_by(gate_args) def _feed_named_rotation_instruction(self, instruction: stim.CircuitInstruction): - layer = self._feed(RotationLayer) + layer = self._feed(LayerRotation) name = instruction.name for t in instruction.targets_copy(): layer.append_named_rotation(name, t.value) def _feed_swap(self, targets: list[stim.GateTarget]): - layer = self._feed(SwapLayer) + layer = self._feed(LayerSwap) for k in range(0, len(targets), 2): layer.targets1.append(targets[k].value) layer.targets2.append(targets[k + 1].value) def _feed_cxswap(self, targets: list[stim.GateTarget]): - layer: InteractSwapLayer = self._feed(InteractSwapLayer) + layer: LayerInteractSwap = self._feed(LayerInteractSwap) for k in range(0, len(targets), 2): layer.i_layer.targets1.append(targets[k].value) layer.i_layer.targets2.append(targets[k + 1].value) @@ -120,7 +120,7 @@ def _feed_cxswap(self, targets: list[stim.GateTarget]): layer.i_layer.bases2.append("X") def _feed_swapcx(self, targets: list[stim.GateTarget]): - layer: InteractSwapLayer = self._feed(InteractSwapLayer) + layer: LayerInteractSwap = self._feed(LayerInteractSwap) for k in range(0, len(targets), 2): layer.i_layer.targets1.append(targets[k].value) layer.i_layer.targets2.append(targets[k + 1].value) @@ -128,13 +128,13 @@ def _feed_swapcx(self, targets: list[stim.GateTarget]): layer.i_layer.bases2.append("Z") def _feed_iswap(self, targets: list[stim.GateTarget]): - layer = self._feed(ISwapLayer) + layer = self._feed(LayerISwap) for k in range(0, len(targets), 2): layer.targets1.append(targets[k].value) layer.targets2.append(targets[k + 1].value) def _feed_sqrt_pp(self, basis: Literal["X", "Y", "Z"], targets: list[stim.GateTarget]): - layer = self._feed(SqrtPPLayer) + layer = self._feed(LayerSqrtPP) for k in range(0, len(targets), 2): layer.targets1.append(targets[k].value) layer.targets2.append(targets[k + 1].value) @@ -148,7 +148,7 @@ def _feed_c( ): is_feedback = any(t.is_sweep_bit_target or t.is_measurement_record_target for t in targets) if is_feedback: - f_layer: FeedbackLayer = self._feed(FeedbackLayer) + f_layer: LayerFeedback = self._feed(LayerFeedback) for k in range(0, len(targets), 2): c = targets[k] t = targets[k + 1] @@ -160,7 +160,7 @@ def _feed_c( f_layer.controls.append(c) f_layer.targets.append(t.value) else: - i_layer: InteractLayer = self._feed(InteractLayer) + i_layer: LayerInteract = self._feed(LayerInteract) for k in range(0, len(targets), 2): i_layer.bases1.append(basis1) i_layer.bases2.append(basis2) @@ -174,7 +174,7 @@ def from_stim_circuit(circuit: stim.Circuit) -> LayerCircuit: gate_data = stim.gate_data(instruction.name) if isinstance(instruction, stim.CircuitRepeatBlock): result.layers.append( - LoopLayer( + LayerLoop( body=LayerCircuit.from_stim_circuit(instruction.body_copy()), repetitions=instruction.repeat_count, ) @@ -248,7 +248,7 @@ def from_stim_circuit(circuit: stim.Circuit) -> LayerCircuit: result._feed_swapcx(instruction.targets_copy()) elif instruction.name == "TICK": - result.layers.append(EmptyLayer()) + result.layers.append(LayerEmpty()) elif instruction.name == "SQRT_XX" or instruction.name == "SQRT_XX_DAG": result._feed_sqrt_pp("X", instruction.targets_copy()) @@ -263,7 +263,7 @@ def from_stim_circuit(circuit: stim.Circuit) -> LayerCircuit: or instruction.name == "Z_ERROR" or instruction.name == "DEPOLARIZE2" ): - result._feed(NoiseLayer).circuit.append(instruction) + result._feed(LayerNoise).circuit.append(instruction) else: raise NotImplementedError(f"{instruction=}") @@ -281,22 +281,22 @@ def __repr__(self) -> str: def with_qubit_coords_at_start(self) -> LayerCircuit: k = len(self.layers) - merged_layer = QubitCoordAnnotationLayer() + merged_layer = LayerQubitCoordAnnotation() rev_layers: list[Layer] = [] while k > 0: k -= 1 layer = self.layers[k] - if isinstance(layer, QubitCoordAnnotationLayer): + if isinstance(layer, LayerQubitCoordAnnotation): intersection = merged_layer.coords.keys() & layer.coords.keys() if intersection: raise ValueError( f"Qubit coords specified twice for qubits {sorted(intersection)}" ) merged_layer.coords.update(layer.coords) - elif isinstance(layer, ShiftCoordAnnotationLayer): + elif isinstance(layer, LayerShiftCoordAnnotation): merged_layer.offset_by(layer.shift) rev_layers.append(layer) - elif isinstance(layer, LoopLayer): + elif isinstance(layer, LayerLoop): if merged_layer.coords: raise NotImplementedError("Moving qubit coords across a loop.") rev_layers.append(layer) @@ -328,9 +328,9 @@ def _resets_at_layer(self, k: int, *, end_resets: set[int]) -> set[int]: return end_resets layer = self.layers[k] - if isinstance(layer, ResetLayer): + if isinstance(layer, LayerReset): return layer.touched() - if isinstance(layer, LoopLayer): + if isinstance(layer, LayerLoop): return layer.body._resets_at_layer(0, end_resets=set()) return set() @@ -354,11 +354,11 @@ def with_rotations_before_resets_removed( new_layers: list[Layer] = [layer.copy() for layer in self.layers] for k, layer in enumerate(new_layers): - if isinstance(layer, LoopLayer): + if isinstance(layer, LayerLoop): layer.body = layer.body.with_rotations_before_resets_removed( loop_boundary_resets=self._resets_at_layer(k + 1, end_resets=all_touched) ) - elif isinstance(layer, RotationLayer): + elif isinstance(layer, LayerRotation): drops = [] for q, gate in layer.named_rotations.items(): if gate != "I": @@ -388,7 +388,7 @@ def scan(qubit: int, start_layer: int, delta: int) -> int | None: if start_layer < 0 or start_layer >= len(sets): return None if ( - isinstance(new_layers[start_layer], RotationLayer) + isinstance(new_layers[start_layer], LayerRotation) and not new_layers[start_layer].is_vacuous() ): return start_layer @@ -399,7 +399,7 @@ def scan(qubit: int, start_layer: int, delta: int) -> int | None: cur_layer_index = 0 while cur_layer_index < len(new_layers): layer = new_layers[cur_layer_index] - if isinstance(layer, RotationLayer): + if isinstance(layer, LayerRotation): rewrites = {} for q, r in layer.named_rotations.items(): if r == "I": @@ -416,7 +416,7 @@ def scan(qubit: int, start_layer: int, delta: int) -> int | None: if r == "I": continue new_layer_index = rewrites[q] - new_layer: RotationLayer = cast(RotationLayer, new_layers[new_layer_index]) + new_layer: LayerRotation = cast(LayerRotation, new_layers[new_layer_index]) if new_layer_index > cur_layer_index: new_layer.prepend_named_rotation(r, q) else: @@ -427,7 +427,7 @@ def scan(qubit: int, start_layer: int, delta: int) -> int | None: sets[new_layer_index].remove(q) layer.named_rotations.clear() sets[cur_layer_index].clear() - elif isinstance(layer, LoopLayer): + elif isinstance(layer, LayerLoop): layer.body = layer.body.with_clearable_rotation_layers_cleared() cur_layer_index += 1 return LayerCircuit([layer for layer in new_layers if not layer.is_vacuous()]) @@ -468,28 +468,28 @@ def with_rotations_rolled_from_end_of_loop_to_start_of_loop(self) -> LayerCircui new_layers: list[Layer] = [] for layer in self.layers: handled = False - if isinstance(layer, LoopLayer): + if isinstance(layer, LayerLoop): loop_layers = list(layer.body.layers) rot_layer_index = len(loop_layers) - 1 while rot_layer_index > 0: if isinstance( loop_layers[rot_layer_index], - (DetObsAnnotationLayer, ShiftCoordAnnotationLayer), + (DetObsAnnotationLayer, LayerShiftCoordAnnotation), ): rot_layer_index -= 1 continue - if isinstance(loop_layers[rot_layer_index], RotationLayer): + if isinstance(loop_layers[rot_layer_index], LayerRotation): break # Loop didn't end with a rotation layer; give up. rot_layer_index = 0 if rot_layer_index > 0: handled = True - popped = cast(RotationLayer, loop_layers.pop(rot_layer_index)) + popped = cast(LayerRotation, loop_layers.pop(rot_layer_index)) loop_layers.insert(0, popped) new_layers.append(popped.inverse()) new_layers.append( - LoopLayer(body=LayerCircuit(loop_layers), repetitions=layer.repetitions) + LayerLoop(body=LayerCircuit(loop_layers), repetitions=layer.repetitions) ) new_layers.append(popped.copy()) if not handled: @@ -505,7 +505,7 @@ def scan(qubit: int, start_layer: int) -> int | None: if start_layer < 0: return None l = new_layers[start_layer] - if isinstance(l, RotationLayer) and qubit in l.named_rotations: + if isinstance(l, LayerRotation) and qubit in l.named_rotations: return start_layer if qubit in sets[start_layer]: return None @@ -514,7 +514,7 @@ def scan(qubit: int, start_layer: int) -> int | None: cur_layer_index = 0 while cur_layer_index < len(new_layers): layer = new_layers[cur_layer_index] - if isinstance(layer, RotationLayer): + if isinstance(layer, LayerRotation): rewrites = {} for q, gate in layer.named_rotations.items(): if gate == "I": @@ -523,28 +523,28 @@ def scan(qubit: int, start_layer: int) -> int | None: if v is not None: rewrites[q] = v for q, dst in rewrites.items(): - new_layer: RotationLayer = cast(RotationLayer, new_layers[dst]) + new_layer: LayerRotation = cast(LayerRotation, new_layers[dst]) new_layer.append_named_rotation(layer.named_rotations.pop(q), q) sets[cur_layer_index].remove(q) if new_layer.named_rotations.get(q): sets[dst].add(q) elif q in sets[dst]: sets[dst].remove(q) - elif isinstance(layer, LoopLayer): + elif isinstance(layer, LayerLoop): layer.body = layer.body.with_rotations_merged_earlier() cur_layer_index += 1 return LayerCircuit([layer for layer in new_layers if not layer.is_vacuous()]) def with_whole_rotation_layers_slid_earlier(self) -> LayerCircuit: rev_layers: list[Layer] = [] - cur_rot_layer: RotationLayer | None = None + cur_rot_layer: LayerRotation | None = None cur_rot_touched: set[int] | None = None for layer in self.layers[::-1]: if cur_rot_layer is not None and not layer.touched().isdisjoint(cur_rot_touched): rev_layers.append(cur_rot_layer) cur_rot_layer = None cur_rot_touched = None - if isinstance(layer, RotationLayer): + if isinstance(layer, LayerRotation): layer = layer.copy() if cur_rot_layer is not None: layer.named_rotations.update(cur_rot_layer.named_rotations) @@ -586,7 +586,7 @@ def with_ejected_loop_iterations(self) -> LayerCircuit: """ new_layers: list[Layer] = [] for layer in self.layers: - if isinstance(layer, LoopLayer): + if isinstance(layer, LayerLoop): if layer.repetitions == 0: pass elif layer.repetitions == 1: @@ -597,7 +597,7 @@ def with_ejected_loop_iterations(self) -> LayerCircuit: else: new_layers.extend(layer.body.layers) new_layers.append( - LoopLayer(body=layer.body.copy(), repetitions=layer.repetitions - 2) + LayerLoop(body=layer.body.copy(), repetitions=layer.repetitions - 2) ) new_layers.extend(layer.body.layers) assert layer.repetitions > 2 @@ -613,10 +613,10 @@ def without_empty_layers(self) -> LayerCircuit: """ new_layers: list[Layer] = [] for layer in self.layers: - if isinstance(layer, EmptyLayer): + if isinstance(layer, LayerEmpty): pass - elif isinstance(layer, LoopLayer): - new_layers.append(LoopLayer(layer.body.without_empty_layers(), layer.repetitions)) + elif isinstance(layer, LayerLoop): + new_layers.append(LayerLoop(layer.body.without_empty_layers(), layer.repetitions)) else: new_layers.append(layer) return LayerCircuit(new_layers) @@ -660,7 +660,7 @@ def with_cleaned_up_loop_iterations(self) -> LayerCircuit: k = 0 while k < len(new_layers): cur_layer = new_layers[k] - if isinstance(cur_layer, LoopLayer): + if isinstance(cur_layer, LayerLoop): body_layers = cur_layer.body.layers reps = cur_layer.repetitions while k >= len(body_layers) and new_layers[k - len(body_layers) : k] == body_layers: @@ -673,7 +673,7 @@ def with_cleaned_up_loop_iterations(self) -> LayerCircuit: ): new_layers[k + 1 : k + 1 + len(body_layers)] = [] reps += 1 - new_layers[k] = LoopLayer(LayerCircuit(body_layers), reps) + new_layers[k] = LayerLoop(LayerCircuit(body_layers), reps) k += 1 return LayerCircuit(new_layers) @@ -701,21 +701,21 @@ def with_locally_merged_measure_layers(self) -> LayerCircuit: k = 0 while k < len(self.layers): cur_layer = self.layers[k] - if isinstance(cur_layer, MeasureLayer): - m1: MeasureLayer = cur_layer + if isinstance(cur_layer, LayerMeasure): + m1: LayerMeasure = cur_layer k2 = k + 1 while k2 < len(self.layers) and isinstance( - self.layers[k2], (DetObsAnnotationLayer, ShiftCoordAnnotationLayer) + self.layers[k2], (DetObsAnnotationLayer, LayerShiftCoordAnnotation) ): k2 += 1 - if k2 < len(self.layers) and isinstance(self.layers[k2], MeasureLayer): - m2: MeasureLayer = cast(MeasureLayer, self.layers[k2]) + if k2 < len(self.layers) and isinstance(self.layers[k2], LayerMeasure): + m2: LayerMeasure = cast(LayerMeasure, self.layers[k2]) if set(m1.targets).isdisjoint(set(m2.targets)): new_layers.append( - MeasureLayer(targets=m1.targets + m2.targets, bases=m1.bases + m2.bases) + LayerMeasure(targets=m1.targets + m2.targets, bases=m1.bases + m2.bases) ) for k3 in range(k + 1, k2): - l3: DetObsAnnotationLayer | ShiftCoordAnnotationLayer + l3: DetObsAnnotationLayer | LayerShiftCoordAnnotation l3 = cast(Any, self.layers[k3]) new_layers.append(l3.with_rec_targets_shifted_by(-len(m2.targets))) k = k2 + 1 @@ -774,14 +774,14 @@ def with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer( def with_irrelevant_tail_layers_removed(self) -> LayerCircuit: irrelevant_layer_types_at_end = ( - ResetLayer, - InteractLayer, - FeedbackLayer, - RotationLayer, - SwapLayer, - ISwapLayer, - InteractSwapLayer, - EmptyLayer, + LayerReset, + LayerInteract, + LayerFeedback, + LayerRotation, + LayerSwap, + LayerISwap, + LayerInteractSwap, + LayerEmpty, ) tail = [] result = list(self.layers) diff --git a/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py b/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py index bcac224d..ff296080 100644 --- a/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py @@ -1,8 +1,8 @@ import stim -from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer_interact import LayerInteract from stimflow._layers._layer_circuit import LayerCircuit -from stimflow._layers._reset_layer import ResetLayer +from stimflow._layers._layer_reset import LayerReset def test_with_squashed_rotations(): @@ -473,8 +473,8 @@ def test_with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer() """ ) ) - .with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type(ResetLayer) - .with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(InteractLayer) + .with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type(LayerReset) + .with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(LayerInteract) == LayerCircuit.from_stim_circuit( stim.Circuit( """ diff --git a/glue/stimflow/src/stimflow/_layers/_det_obs_annotation_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_det_obs_annotation.py similarity index 100% rename from glue/stimflow/src/stimflow/_layers/_det_obs_annotation_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_det_obs_annotation.py diff --git a/glue/stimflow/src/stimflow/_layers/_empty_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_empty.py similarity index 83% rename from glue/stimflow/src/stimflow/_layers/_empty_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_empty.py index 06d91690..a62e3f49 100644 --- a/glue/stimflow/src/stimflow/_layers/_empty_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_empty.py @@ -8,9 +8,9 @@ @dataclasses.dataclass -class EmptyLayer(Layer): - def copy(self) -> EmptyLayer: - return EmptyLayer() +class LayerEmpty(Layer): + def copy(self) -> LayerEmpty: + return LayerEmpty() def touched(self) -> set[int]: return set() diff --git a/glue/stimflow/src/stimflow/_layers/_feedback_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_feedback.py similarity index 83% rename from glue/stimflow/src/stimflow/_layers/_feedback_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_feedback.py index eaa00807..10b94e2b 100644 --- a/glue/stimflow/src/stimflow/_layers/_feedback_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_feedback.py @@ -9,22 +9,22 @@ from stimflow._layers._layer import Layer if TYPE_CHECKING: - from stimflow._layers._rotation_layer import RotationLayer + from stimflow._layers._rotation_layer import LayerRotation @dataclasses.dataclass -class FeedbackLayer(Layer): +class LayerFeedback(Layer): controls: list[stim.GateTarget] = dataclasses.field(default_factory=list) targets: list[int] = dataclasses.field(default_factory=list) bases: list[Literal["X", "Y", "Z"]] = dataclasses.field(default_factory=list) - def with_rec_targets_shifted_by(self, shift: int) -> FeedbackLayer: + def with_rec_targets_shifted_by(self, shift: int) -> LayerFeedback: result = self.copy() result.controls = [stim.target_rec(t.value + shift) for t in result.controls] return result - def copy(self) -> FeedbackLayer: - return FeedbackLayer( + def copy(self) -> LayerFeedback: + return LayerFeedback( targets=list(self.targets), controls=list(self.controls), bases=list(self.bases) ) @@ -37,8 +37,8 @@ def requires_tick_before(self) -> bool: def implies_eventual_tick_after(self) -> bool: return False - def before(self, layer: RotationLayer) -> FeedbackLayer: - return FeedbackLayer( + def before(self, layer: LayerRotation) -> LayerFeedback: + return LayerFeedback( controls=list(self.controls), targets=list(self.targets), bases=[ diff --git a/glue/stimflow/src/stimflow/_layers/_feedback_layer_test.py b/glue/stimflow/src/stimflow/_layers/_layer_feedback_test.py similarity index 76% rename from glue/stimflow/src/stimflow/_layers/_feedback_layer_test.py rename to glue/stimflow/src/stimflow/_layers/_layer_feedback_test.py index 9aef7b43..97e4f540 100644 --- a/glue/stimflow/src/stimflow/_layers/_feedback_layer_test.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_feedback_test.py @@ -1,4 +1,4 @@ -from stimflow._layers._feedback_layer import _basis_before_rotation +from stimflow._layers._layer_feedback import _basis_before_rotation def test_basis_before_rotation(): diff --git a/glue/stimflow/src/stimflow/_layers/_interact_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_interact.py similarity index 83% rename from glue/stimflow/src/stimflow/_layers/_interact_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_interact.py index b136c710..0e2c154b 100644 --- a/glue/stimflow/src/stimflow/_layers/_interact_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_interact.py @@ -9,7 +9,7 @@ @dataclasses.dataclass -class InteractLayer(Layer): +class LayerInteract(Layer): """A layer of controlled Pauli gates (like CX, CZ, and XCY).""" targets1: list[int] = dataclasses.field(default_factory=list) @@ -20,8 +20,8 @@ class InteractLayer(Layer): def touched(self) -> set[int]: return set(self.targets1 + self.targets2) - def copy(self) -> InteractLayer: - return InteractLayer( + def copy(self) -> LayerInteract: + return LayerInteract( targets1=list(self.targets1), targets2=list(self.targets2), bases1=list(self.bases1), @@ -29,9 +29,9 @@ def copy(self) -> InteractLayer: ) def rotate_to_z_layer(self): - from stimflow._layers._rotation_layer import RotationLayer + from stimflow._layers._layer_rotation import LayerRotation - result = RotationLayer() + result = LayerRotation() for targets, bases in [(self.targets1, self.bases1), (self.targets2, self.bases2)]: for q, b in zip(targets, bases): if b == "X": @@ -44,7 +44,7 @@ def to_z_basis(self) -> list[Layer]: rot = self.rotate_to_z_layer() return [ rot, - InteractLayer( + LayerInteract( targets1=list(self.targets1), targets2=list(self.targets2), bases1=["Z"] * len(self.targets1), @@ -70,19 +70,19 @@ def append_into_stim_circuit(self, out: stim.Circuit) -> None: out.append(gate, pair) def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: - from stimflow._layers._interact_swap_layer import InteractSwapLayer - from stimflow._layers._swap_layer import SwapLayer + from stimflow._layers._layer_interact_swap import LayerInteractSwap + from stimflow._layers._layer_swap import LayerSwap - if isinstance(next_layer, SwapLayer): + if isinstance(next_layer, LayerSwap): pairs1 = {frozenset([a, b]) for a, b in zip(self.targets1, self.targets2)} pairs2 = {frozenset([a, b]) for a, b in zip(next_layer.targets1, next_layer.targets2)} if pairs1 == pairs2: - return [InteractSwapLayer(i_layer=self.copy())] - elif isinstance(next_layer, InteractLayer) and self.touched().isdisjoint( + return [LayerInteractSwap(i_layer=self.copy())] + elif isinstance(next_layer, LayerInteract) and self.touched().isdisjoint( next_layer.touched() ): return [ - InteractLayer( + LayerInteract( targets1=self.targets1 + next_layer.targets1, targets2=self.targets2 + next_layer.targets2, bases1=self.bases1 + next_layer.bases1, diff --git a/glue/stimflow/src/stimflow/_layers/_interact_swap_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap.py similarity index 80% rename from glue/stimflow/src/stimflow/_layers/_interact_swap_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_interact_swap.py index 94f711a1..d913846b 100644 --- a/glue/stimflow/src/stimflow/_layers/_interact_swap_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap.py @@ -4,17 +4,17 @@ import stim -from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer_interact import LayerInteract from stimflow._layers._layer import Layer -from stimflow._layers._swap_layer import SwapLayer +from stimflow._layers._layer_swap import LayerSwap @dataclasses.dataclass -class InteractSwapLayer(Layer): - i_layer: InteractLayer = dataclasses.field(default_factory=InteractLayer) +class LayerInteractSwap(Layer): + i_layer: LayerInteract = dataclasses.field(default_factory=LayerInteract) - def copy(self) -> InteractSwapLayer: - return InteractSwapLayer(i_layer=self.i_layer.copy()) + def copy(self) -> LayerInteractSwap: + return LayerInteractSwap(i_layer=self.i_layer.copy()) def touched(self) -> set[int]: return self.i_layer.touched() @@ -22,7 +22,7 @@ def touched(self) -> set[int]: def append_into_stim_circuit(self, out: stim.Circuit) -> None: cz_swaps = [] cx_swaps = [] - reduced_layer = InteractLayer() + reduced_layer = LayerInteract() for t1, t2, b1, b2 in zip( self.i_layer.targets1, self.i_layer.targets2, self.i_layer.bases1, self.i_layer.bases2 ): @@ -50,20 +50,20 @@ def append_into_stim_circuit(self, out: stim.Circuit) -> None: if reduced_layer.targets1: reduced_layer.append_into_stim_circuit(out) out.append("TICK") - SwapLayer(reduced_layer.targets1, reduced_layer.targets2).append_into_stim_circuit(out) + LayerSwap(reduced_layer.targets1, reduced_layer.targets2).append_into_stim_circuit(out) def to_z_basis(self) -> list[Layer]: return [ self.i_layer.rotate_to_z_layer(), - InteractSwapLayer( - InteractLayer( + LayerInteractSwap( + LayerInteract( targets1=list(self.i_layer.targets1), targets2=list(self.i_layer.targets2), bases1=["Z"] * len(self.i_layer.bases1), bases2=["Z"] * len(self.i_layer.bases2), ) ), - InteractLayer( + LayerInteract( targets1=self.i_layer.targets1, targets2=self.i_layer.targets2, bases1=self.i_layer.bases2, diff --git a/glue/stimflow/src/stimflow/_layers/_interact_swap_layer_test.py b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap_test.py similarity index 66% rename from glue/stimflow/src/stimflow/_layers/_interact_swap_layer_test.py rename to glue/stimflow/src/stimflow/_layers/_layer_interact_swap_test.py index 4f6f6177..8f281a6c 100644 --- a/glue/stimflow/src/stimflow/_layers/_interact_swap_layer_test.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap_test.py @@ -1,34 +1,34 @@ import stim -from stimflow._layers._interact_layer import InteractLayer -from stimflow._layers._interact_swap_layer import InteractSwapLayer -from stimflow._layers._rotation_layer import RotationLayer +from stimflow._layers._layer_interact import LayerInteract +from stimflow._layers._layer_interact_swap import LayerInteractSwap +from stimflow._layers._layer_rotation import LayerRotation def test_to_z_basis(): - layer = InteractSwapLayer( - i_layer=InteractLayer( + layer = LayerInteractSwap( + i_layer=LayerInteract( targets1=[0, 2, 4], targets2=[1, 3, 5], bases1=["X", "Z", "Z"], bases2=["Y", "X", "Z"] ) ) v = layer.to_z_basis() assert v == [ - RotationLayer({0: "H", 1: "H_YZ", 3: "H"}), - InteractSwapLayer( - i_layer=InteractLayer( + LayerRotation({0: "H", 1: "H_YZ", 3: "H"}), + LayerInteractSwap( + i_layer=LayerInteract( targets1=[0, 2, 4], targets2=[1, 3, 5], bases1=["Z", "Z", "Z"], bases2=["Z", "Z", "Z"], ) ), - RotationLayer({0: "H_YZ", 1: "H", 2: "H"}), + LayerRotation({0: "H_YZ", 1: "H", 2: "H"}), ] def test_append_into_circuit(): - layer = InteractSwapLayer( - i_layer=InteractLayer( + layer = LayerInteractSwap( + i_layer=LayerInteract( targets1=[0, 2, 4], targets2=[1, 3, 5], bases1=["X", "Z", "Z"], bases2=["Y", "X", "Z"] ) ) @@ -44,8 +44,8 @@ def test_append_into_circuit(): """ ) - layer = InteractSwapLayer( - i_layer=InteractLayer( + layer = LayerInteractSwap( + i_layer=LayerInteract( targets1=[0, 2, 4], targets2=[1, 3, 5], bases1=["Z", "Z", "Z"], bases2=["Z", "X", "Z"] ) ) diff --git a/glue/stimflow/src/stimflow/_layers/_iswap_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_iswap.py similarity index 87% rename from glue/stimflow/src/stimflow/_layers/_iswap_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_iswap.py index b3846c80..5a5e0894 100644 --- a/glue/stimflow/src/stimflow/_layers/_iswap_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_iswap.py @@ -8,14 +8,14 @@ @dataclasses.dataclass -class ISwapLayer(Layer): +class LayerISwap(Layer): """A layer of iswap gates.""" targets1: list[int] = dataclasses.field(default_factory=list) targets2: list[int] = dataclasses.field(default_factory=list) - def copy(self) -> ISwapLayer: - return ISwapLayer(targets1=list(self.targets1), targets2=list(self.targets2)) + def copy(self) -> LayerISwap: + return LayerISwap(targets1=list(self.targets1), targets2=list(self.targets2)) def touched(self) -> set[int]: return set(self.targets1 + self.targets2) diff --git a/glue/stimflow/src/stimflow/_layers/_loop_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_loop.py similarity index 81% rename from glue/stimflow/src/stimflow/_layers/_loop_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_loop.py index 03c25ec4..9530c88a 100644 --- a/glue/stimflow/src/stimflow/_layers/_loop_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_loop.py @@ -12,21 +12,21 @@ @dataclasses.dataclass -class LoopLayer(Layer): +class LayerLoop(Layer): body: LayerCircuit repetitions: int - def copy(self) -> LoopLayer: - return LoopLayer(body=self.body.copy(), repetitions=self.repetitions) + def copy(self) -> LayerLoop: + return LayerLoop(body=self.body.copy(), repetitions=self.repetitions) def touched(self) -> set[int]: return self.body.touched() def to_z_basis(self) -> list[Layer]: - return [LoopLayer(body=self.body.to_z_basis(), repetitions=self.repetitions)] + return [LayerLoop(body=self.body.to_z_basis(), repetitions=self.repetitions)] def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: - optimized = LoopLayer( + optimized = LayerLoop( body=self.body.with_locally_optimized_layers(), repetitions=self.repetitions ) return [optimized, next_layer] diff --git a/glue/stimflow/src/stimflow/_layers/_measure_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_measure.py similarity index 75% rename from glue/stimflow/src/stimflow/_layers/_measure_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_measure.py index 620c01b2..1e262183 100644 --- a/glue/stimflow/src/stimflow/_layers/_measure_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_measure.py @@ -5,24 +5,24 @@ import stim from stimflow._layers._layer import Layer -from stimflow._layers._rotation_layer import RotationLayer +from stimflow._layers._layer_rotation import LayerRotation @dataclasses.dataclass -class MeasureLayer(Layer): +class LayerMeasure(Layer): """A layer of single qubit Pauli basis measurement operations.""" targets: list[int] = dataclasses.field(default_factory=list) bases: list[str] = dataclasses.field(default_factory=list) - def copy(self) -> MeasureLayer: - return MeasureLayer(targets=list(self.targets), bases=list(self.bases)) + def copy(self) -> LayerMeasure: + return LayerMeasure(targets=list(self.targets), bases=list(self.bases)) def touched(self) -> set[int]: return set(self.targets) def to_z_basis(self) -> list[Layer]: - rot = RotationLayer( + rot = LayerRotation( { q: "I" if b == "Z" else "H" if b == "X" else "H_YZ" for q, b in zip(self.targets, self.bases) @@ -30,7 +30,7 @@ def to_z_basis(self) -> list[Layer]: ) return [ rot, - MeasureLayer(targets=list(self.targets), bases=["Z"] * len(self.targets)), + LayerMeasure(targets=list(self.targets), bases=["Z"] * len(self.targets)), rot.copy(), ] @@ -39,15 +39,15 @@ def append_into_stim_circuit(self, out: stim.Circuit) -> None: out.append("M" + b, [t]) def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: - if isinstance(next_layer, MeasureLayer) and set(self.targets).isdisjoint( + if isinstance(next_layer, LayerMeasure) and set(self.targets).isdisjoint( next_layer.targets ): return [ - MeasureLayer( + LayerMeasure( targets=self.targets + next_layer.targets, bases=self.bases + next_layer.bases ) ] - if isinstance(next_layer, RotationLayer) and set(self.targets).isdisjoint( + if isinstance(next_layer, LayerRotation) and set(self.targets).isdisjoint( next_layer.named_rotations.keys() ): return [next_layer, self] diff --git a/glue/stimflow/src/stimflow/_layers/_mpp_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_mpp.py similarity index 85% rename from glue/stimflow/src/stimflow/_layers/_mpp_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_mpp.py index 8ee4ff59..d23b9370 100644 --- a/glue/stimflow/src/stimflow/_layers/_mpp_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_mpp.py @@ -8,11 +8,11 @@ @dataclasses.dataclass -class MppLayer(Layer): +class LayerMpp(Layer): targets: list[list[stim.GateTarget]] = dataclasses.field(default_factory=list) - def copy(self) -> MppLayer: - return MppLayer(targets=[list(e) for e in self.targets]) + def copy(self) -> LayerMpp: + return LayerMpp(targets=[list(e) for e in self.targets]) def touched(self) -> set[int]: return {t.value for mpp in self.targets for t in mpp} diff --git a/glue/stimflow/src/stimflow/_layers/_noise_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_noise.py similarity index 85% rename from glue/stimflow/src/stimflow/_layers/_noise_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_noise.py index ad2b6a90..756fb257 100644 --- a/glue/stimflow/src/stimflow/_layers/_noise_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_noise.py @@ -8,13 +8,13 @@ @dataclasses.dataclass -class NoiseLayer(Layer): +class LayerNoise(Layer): """A layer of noise operations.""" circuit: stim.Circuit = dataclasses.field(default_factory=stim.Circuit) - def copy(self) -> NoiseLayer: - return NoiseLayer(circuit=self.circuit.copy()) + def copy(self) -> LayerNoise: + return LayerNoise(circuit=self.circuit.copy()) def touched(self) -> set[int]: return { diff --git a/glue/stimflow/src/stimflow/_layers/_qubit_coord_annotation_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_qubit_coord_annotation.py similarity index 89% rename from glue/stimflow/src/stimflow/_layers/_qubit_coord_annotation_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_qubit_coord_annotation.py index 44e1de14..ce270c92 100644 --- a/glue/stimflow/src/stimflow/_layers/_qubit_coord_annotation_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_qubit_coord_annotation.py @@ -9,7 +9,7 @@ @dataclasses.dataclass -class QubitCoordAnnotationLayer(Layer): +class LayerQubitCoordAnnotation(Layer): coords: dict[int, list[float]] = dataclasses.field(default_factory=dict) def offset_by(self, args: Iterable[float]): @@ -20,7 +20,7 @@ def offset_by(self, args: Iterable[float]): qubit_coords[index] += offset def copy(self) -> Layer: - return QubitCoordAnnotationLayer(coords=dict(self.coords)) + return LayerQubitCoordAnnotation(coords=dict(self.coords)) def touched(self) -> set[int]: return set() diff --git a/glue/stimflow/src/stimflow/_layers/_reset_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_reset.py similarity index 78% rename from glue/stimflow/src/stimflow/_layers/_reset_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_reset.py index f3ca1c54..f4941f8c 100644 --- a/glue/stimflow/src/stimflow/_layers/_reset_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_reset.py @@ -6,25 +6,25 @@ import stim from stimflow._layers._layer import Layer -from stimflow._layers._rotation_layer import RotationLayer +from stimflow._layers._layer_rotation import LayerRotation @dataclasses.dataclass -class ResetLayer(Layer): +class LayerReset(Layer): """A layer of reset gates.""" targets: dict[int, Literal["X", "Y", "Z"]] = dataclasses.field(default_factory=dict) - def copy(self) -> ResetLayer: - return ResetLayer(targets=dict(self.targets)) + def copy(self) -> LayerReset: + return LayerReset(targets=dict(self.targets)) def touched(self) -> set[int]: return set(self.targets.keys()) def to_z_basis(self) -> list[Layer]: return [ - ResetLayer(targets={q: "Z" for q in self.targets.keys()}), - RotationLayer( + LayerReset(targets={q: "Z" for q in self.targets.keys()}), + LayerRotation( { q: "I" if b == "Z" else "H" if b == "X" else "H_YZ" for q, b in self.targets.items() @@ -42,9 +42,9 @@ def append_into_stim_circuit(self, out: stim.Circuit) -> None: out.append("R" + basis, vs) def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: - if isinstance(next_layer, ResetLayer): + if isinstance(next_layer, LayerReset): return [ - ResetLayer( + LayerReset( targets={t: b for layer in [self, next_layer] for t, b in layer.targets.items()} ) ] diff --git a/glue/stimflow/src/stimflow/_layers/_rotation_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_rotation.py similarity index 77% rename from glue/stimflow/src/stimflow/_layers/_rotation_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_rotation.py index 7c1b247a..472c5568 100644 --- a/glue/stimflow/src/stimflow/_layers/_rotation_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_rotation.py @@ -12,7 +12,7 @@ @dataclasses.dataclass -class RotationLayer(Layer): +class LayerRotation(Layer): """A layer of single qubit Clifford rotation gates.""" named_rotations: dict[int, str] = dataclasses.field(default_factory=dict) @@ -20,12 +20,12 @@ class RotationLayer(Layer): def touched(self) -> set[int]: return {k for k, v in self.named_rotations.items() if v != "I"} - def copy(self) -> RotationLayer: - return RotationLayer(dict(self.named_rotations)) + def copy(self) -> LayerRotation: + return LayerRotation(dict(self.named_rotations)) - def inverse(self) -> RotationLayer: + def inverse(self) -> LayerRotation: t = single_qubit_clifford_inverse_table() - return RotationLayer({q: t[r] for q, r in self.named_rotations.items()}) + return LayerRotation({q: t[r] for q, r in self.named_rotations.items()}) def append_into_stim_circuit(self, out: stim.Circuit) -> None: gate2targets: dict[str, list[int]] = {} @@ -64,16 +64,16 @@ def is_vacuous(self) -> bool: return not any(self.named_rotations.values()) def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: - from stimflow._layers._det_obs_annotation_layer import DetObsAnnotationLayer - from stimflow._layers._feedback_layer import FeedbackLayer - from stimflow._layers._reset_layer import ResetLayer - from stimflow._layers._shift_coord_annotation_layer import ShiftCoordAnnotationLayer + from stimflow._layers._layer_det_obs_annotation import DetObsAnnotationLayer + from stimflow._layers._layer_feedback import LayerFeedback + from stimflow._layers._layer_reset import LayerReset + from stimflow._layers._layer_shift_coord_annotation import LayerShiftCoordAnnotation - if isinstance(next_layer, (DetObsAnnotationLayer, ShiftCoordAnnotationLayer)): + if isinstance(next_layer, (DetObsAnnotationLayer, LayerShiftCoordAnnotation)): return [next_layer, self] - if isinstance(next_layer, FeedbackLayer): + if isinstance(next_layer, LayerFeedback): return [next_layer.before(self), self] - if isinstance(next_layer, ResetLayer): + if isinstance(next_layer, LayerReset): trimmed = self.copy() for t in next_layer.targets.keys(): trimmed.named_rotations.pop(t, None) @@ -81,7 +81,7 @@ def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: return [trimmed, next_layer] else: return [next_layer] - if isinstance(next_layer, RotationLayer): + if isinstance(next_layer, LayerRotation): result = self.copy() for q, r in next_layer.named_rotations.items(): result.append_named_rotation(r, q) diff --git a/glue/stimflow/src/stimflow/_layers/_rotation_layer_test.py b/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py similarity index 90% rename from glue/stimflow/src/stimflow/_layers/_rotation_layer_test.py rename to glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py index e088a3c0..001df212 100644 --- a/glue/stimflow/src/stimflow/_layers/_rotation_layer_test.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py @@ -6,14 +6,14 @@ def test_fuses_rotations(): - layer = stimflow.RotationLayer() + layer = stimflow.LayerRotation() layer.append_named_rotation("H", 0) layer.append_named_rotation("H_NXZ", 0) assert layer.named_rotations[0] == "Y" def test_output(): - layer = stimflow.RotationLayer() + layer = stimflow.LayerRotation() layer.append_named_rotation("H", 0) layer.append_named_rotation("C_XYZ", 0) diff --git a/glue/stimflow/src/stimflow/_layers/_shift_coord_annotation_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_shift_coord_annotation.py similarity index 77% rename from glue/stimflow/src/stimflow/_layers/_shift_coord_annotation_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_shift_coord_annotation.py index 09a7dac1..09728802 100644 --- a/glue/stimflow/src/stimflow/_layers/_shift_coord_annotation_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_shift_coord_annotation.py @@ -9,7 +9,7 @@ @dataclasses.dataclass -class ShiftCoordAnnotationLayer(Layer): +class LayerShiftCoordAnnotation(Layer): shift: list[float] = dataclasses.field(default_factory=list) def offset_by(self, args: Iterable[float]): @@ -19,8 +19,8 @@ def offset_by(self, args: Iterable[float]): else: self.shift[k] += arg - def copy(self) -> ShiftCoordAnnotationLayer: - return ShiftCoordAnnotationLayer(shift=self.shift) + def copy(self) -> LayerShiftCoordAnnotation: + return LayerShiftCoordAnnotation(shift=self.shift) def touched(self) -> set[int]: return set() @@ -35,11 +35,11 @@ def append_into_stim_circuit(self, out: stim.Circuit) -> None: out.append("SHIFT_COORDS", [], self.shift) def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: - if isinstance(next_layer, ShiftCoordAnnotationLayer): + if isinstance(next_layer, LayerShiftCoordAnnotation): result = self.copy() result.offset_by(next_layer.shift) return [result] return [self, next_layer] - def with_rec_targets_shifted_by(self, shift: int) -> ShiftCoordAnnotationLayer: + def with_rec_targets_shifted_by(self, shift: int) -> LayerShiftCoordAnnotation: return self.copy() diff --git a/glue/stimflow/src/stimflow/_layers/_sqrt_pp_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_sqrt_pp.py similarity index 85% rename from glue/stimflow/src/stimflow/_layers/_sqrt_pp_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_sqrt_pp.py index 854d9c75..2fdac1a2 100644 --- a/glue/stimflow/src/stimflow/_layers/_sqrt_pp_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_sqrt_pp.py @@ -5,13 +5,13 @@ import stim -from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer_interact import LayerInteract from stimflow._layers._layer import Layer -from stimflow._layers._rotation_layer import RotationLayer +from stimflow._layers._layer_rotation import LayerRotation @dataclasses.dataclass -class SqrtPPLayer(Layer): +class LayerSqrtPP(Layer): targets1: list[int] = dataclasses.field(default_factory=list) targets2: list[int] = dataclasses.field(default_factory=list) bases: list[str] = dataclasses.field(default_factory=list) @@ -19,14 +19,14 @@ class SqrtPPLayer(Layer): def touched(self) -> set[int]: return set(self.targets1 + self.targets2) - def copy(self) -> SqrtPPLayer: - return SqrtPPLayer( + def copy(self) -> LayerSqrtPP: + return LayerSqrtPP( targets1=list(self.targets1), targets2=list(self.targets2), bases=list(self.bases) ) def to_z_basis(self) -> list[Layer]: - interact = InteractLayer() - rot = RotationLayer() + interact = LayerInteract() + rot = LayerRotation() for q1, q2, b in zip(self.targets1, self.targets2, self.bases): interact.targets1.append(q1) interact.targets2.append(q2) diff --git a/glue/stimflow/src/stimflow/_layers/_swap_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_swap.py similarity index 77% rename from glue/stimflow/src/stimflow/_layers/_swap_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_swap.py index 0c6ca132..fad58910 100644 --- a/glue/stimflow/src/stimflow/_layers/_swap_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_swap.py @@ -4,14 +4,14 @@ import stim -from stimflow._layers._det_obs_annotation_layer import DetObsAnnotationLayer -from stimflow._layers._interact_layer import InteractLayer +from stimflow._layers._layer_det_obs_annotation import DetObsAnnotationLayer +from stimflow._layers._layer_interact import LayerInteract from stimflow._layers._layer import Layer -from stimflow._layers._shift_coord_annotation_layer import ShiftCoordAnnotationLayer +from stimflow._layers._layer_shift_coord_annotation import LayerShiftCoordAnnotation @dataclasses.dataclass -class SwapLayer(Layer): +class LayerSwap(Layer): """A layer of swap gates.""" targets1: list[int] = dataclasses.field(default_factory=list) @@ -27,8 +27,8 @@ def to_swap_dict(self) -> dict[int, int]: d[b] = a return d - def copy(self) -> SwapLayer: - return SwapLayer(targets1=list(self.targets1), targets2=list(self.targets2)) + def copy(self) -> LayerSwap: + return LayerSwap(targets1=list(self.targets1), targets2=list(self.targets2)) def append_into_stim_circuit(self, out: stim.Circuit) -> None: pairs = [] @@ -41,20 +41,20 @@ def append_into_stim_circuit(self, out: stim.Circuit) -> None: out.append("SWAP", pair) def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: - if isinstance(next_layer, InteractLayer): - from stimflow._layers._interact_swap_layer import InteractSwapLayer + if isinstance(next_layer, LayerInteract): + from stimflow._layers._layer_interact_swap import LayerInteractSwap pairs1 = {frozenset([a, b]) for a, b in zip(self.targets1, self.targets2)} pairs2 = {frozenset([a, b]) for a, b in zip(next_layer.targets1, next_layer.targets2)} if pairs1 == pairs2: i = next_layer.copy() i.targets1, i.targets2 = i.targets2, i.targets1 - return [InteractSwapLayer(i_layer=i)] - if isinstance(next_layer, (ShiftCoordAnnotationLayer, DetObsAnnotationLayer)): + return [LayerInteractSwap(i_layer=i)] + if isinstance(next_layer, (LayerShiftCoordAnnotation, DetObsAnnotationLayer)): return [next_layer, self] - if isinstance(next_layer, SwapLayer): + if isinstance(next_layer, LayerSwap): total_swaps = self.to_swap_dict() - leftover_swaps = SwapLayer() + leftover_swaps = LayerSwap() for a, b in zip(next_layer.targets1, next_layer.targets2): a2 = total_swaps.get(a) b2 = total_swaps.get(b) @@ -69,7 +69,7 @@ def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: leftover_swaps.targets2.append(b) result: list[Layer | None] = [] if total_swaps: - new_layer = SwapLayer() + new_layer = LayerSwap() for k, v in total_swaps.items(): if k < v: new_layer.targets1.append(k) diff --git a/glue/stimflow/src/stimflow/_layers/_tag_layer.py b/glue/stimflow/src/stimflow/_layers/_layer_tag.py similarity index 89% rename from glue/stimflow/src/stimflow/_layers/_tag_layer.py rename to glue/stimflow/src/stimflow/_layers/_layer_tag.py index d6621616..e858ca74 100644 --- a/glue/stimflow/src/stimflow/_layers/_tag_layer.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_tag.py @@ -8,11 +8,11 @@ @dataclasses.dataclass -class TagLayer(Layer): +class LayerTag(Layer): circuit: stim.Circuit = dataclasses.field(default_factory=stim.Circuit) - def copy(self) -> TagLayer: - return TagLayer(circuit=self.circuit) + def copy(self) -> LayerTag: + return LayerTag(circuit=self.circuit) def touched(self) -> set[int]: # set of qubit touched by it tagged_gate_targets = self.circuit[0].target_groups()[0] diff --git a/glue/stimflow/src/stimflow/_layers/_tag_layer_test.py b/glue/stimflow/src/stimflow/_layers/_layer_tag_test.py similarity index 100% rename from glue/stimflow/src/stimflow/_layers/_tag_layer_test.py rename to glue/stimflow/src/stimflow/_layers/_layer_tag_test.py From 11195a3e1593a231806af085cb4ef8a06a573e20 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 15:58:47 -0700 Subject: [PATCH 6/9] Drop `compile_chunks_into_circuit` and `circuit_to_cycle_code_slices` Remove the layer objects from top level --- glue/stimflow/doc/api.md | 454 +++--------------- glue/stimflow/src/stimflow/__init__.py | 6 - glue/stimflow/src/stimflow/_chunk/__init__.py | 3 +- .../src/stimflow/_chunk/_chunk_builder.py | 80 ++- .../src/stimflow/_chunk/_chunk_compiler.py | 39 -- .../stimflow/_chunk/_chunk_compiler_test.py | 78 ++- .../src/stimflow/_chunk/_chunk_reflow.py | 3 + .../src/stimflow/_chunk/_chunk_test.py | 6 +- .../src/stimflow/_chunk/_code_util.py | 40 -- .../stimflow/_layers/_layer_rotation_test.py | 6 +- 10 files changed, 201 insertions(+), 514 deletions(-) diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 14a2acea..2c3d25af 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -119,34 +119,6 @@ - [`stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type`](#stimflow.LayerCircuit.with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type) - [`stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier`](#stimflow.LayerCircuit.with_whole_rotation_layers_slid_earlier) - [`stimflow.LayerCircuit.without_empty_layers`](#stimflow.LayerCircuit.without_empty_layers) -- [`stimflow.LayerInteract`](#stimflow.LayerInteract) - - [`stimflow.LayerInteract.append_into_stim_circuit`](#stimflow.LayerInteract.append_into_stim_circuit) - - [`stimflow.LayerInteract.copy`](#stimflow.LayerInteract.copy) - - [`stimflow.LayerInteract.locally_optimized`](#stimflow.LayerInteract.locally_optimized) - - [`stimflow.LayerInteract.rotate_to_z_layer`](#stimflow.LayerInteract.rotate_to_z_layer) - - [`stimflow.LayerInteract.to_z_basis`](#stimflow.LayerInteract.to_z_basis) - - [`stimflow.LayerInteract.touched`](#stimflow.LayerInteract.touched) -- [`stimflow.LayerMeasure`](#stimflow.LayerMeasure) - - [`stimflow.LayerMeasure.append_into_stim_circuit`](#stimflow.LayerMeasure.append_into_stim_circuit) - - [`stimflow.LayerMeasure.copy`](#stimflow.LayerMeasure.copy) - - [`stimflow.LayerMeasure.locally_optimized`](#stimflow.LayerMeasure.locally_optimized) - - [`stimflow.LayerMeasure.to_z_basis`](#stimflow.LayerMeasure.to_z_basis) - - [`stimflow.LayerMeasure.touched`](#stimflow.LayerMeasure.touched) -- [`stimflow.LayerReset`](#stimflow.LayerReset) - - [`stimflow.LayerReset.append_into_stim_circuit`](#stimflow.LayerReset.append_into_stim_circuit) - - [`stimflow.LayerReset.copy`](#stimflow.LayerReset.copy) - - [`stimflow.LayerReset.locally_optimized`](#stimflow.LayerReset.locally_optimized) - - [`stimflow.LayerReset.to_z_basis`](#stimflow.LayerReset.to_z_basis) - - [`stimflow.LayerReset.touched`](#stimflow.LayerReset.touched) -- [`stimflow.LayerRotation`](#stimflow.LayerRotation) - - [`stimflow.LayerRotation.append_into_stim_circuit`](#stimflow.LayerRotation.append_into_stim_circuit) - - [`stimflow.LayerRotation.append_named_rotation`](#stimflow.LayerRotation.append_named_rotation) - - [`stimflow.LayerRotation.copy`](#stimflow.LayerRotation.copy) - - [`stimflow.LayerRotation.inverse`](#stimflow.LayerRotation.inverse) - - [`stimflow.LayerRotation.is_vacuous`](#stimflow.LayerRotation.is_vacuous) - - [`stimflow.LayerRotation.locally_optimized`](#stimflow.LayerRotation.locally_optimized) - - [`stimflow.LayerRotation.prepend_named_rotation`](#stimflow.LayerRotation.prepend_named_rotation) - - [`stimflow.LayerRotation.touched`](#stimflow.LayerRotation.touched) - [`stimflow.LineDataFor3DModel`](#stimflow.LineDataFor3DModel) - [`stimflow.LineDataFor3DModel.__init__`](#stimflow.LineDataFor3DModel.__init__) - [`stimflow.LineDataFor3DModel.fused`](#stimflow.LineDataFor3DModel.fused) @@ -250,10 +222,8 @@ - [`stimflow.Viewable3dModelGLTF`](#stimflow.Viewable3dModelGLTF) - [`stimflow.Viewable3dModelGLTF.html_viewer`](#stimflow.Viewable3dModelGLTF.html_viewer) - [`stimflow.append_reindexed_content_to_circuit`](#stimflow.append_reindexed_content_to_circuit) -- [`stimflow.circuit_to_cycle_code_slices`](#stimflow.circuit_to_cycle_code_slices) - [`stimflow.circuit_to_dem_target_measurement_records_map`](#stimflow.circuit_to_dem_target_measurement_records_map) - [`stimflow.circuit_with_xz_flipped`](#stimflow.circuit_with_xz_flipped) -- [`stimflow.compile_chunks_into_circuit`](#stimflow.compile_chunks_into_circuit) - [`stimflow.count_measurement_layers`](#stimflow.count_measurement_layers) - [`stimflow.find_d1_error`](#stimflow.find_d1_error) - [`stimflow.find_d2_error`](#stimflow.find_d2_error) @@ -703,10 +673,77 @@ def with_xz_flipped( # (at top-level in the stimflow module) class ChunkBuilder: - """Helper class for building stim circuits. + """A helper class for building chunks. - Handles qubit indexing (complex -> int). - Handles measurement tracking (naming results and referring to them by name). + This class takes care of details like converting qubit coordinates into qubit indices, + storing and retrieving measurement indices, and accumulating flow data. + + Example: + >>> import stimflow as sf + + >>> # Build a repetition code idling chunk. + >>> d = 5 + >>> data_qubits = range(d) + >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] + >>> builder = sf.ChunkBuilder() + >>> builder.append("R", measure_qubits) + >>> builder.append("TICK") + >>> builder.append("CX", [(m-0.5, m) for m in measure_qubits]) + >>> builder.append("TICK") + >>> builder.append("CX", [(m+0.5, m) for m in measure_qubits]) + >>> builder.append("TICK") + >>> builder.append("M", measure_qubits) + >>> for m in measure_qubits: + ... stabilizer = sf.PauliMap.from_zs([m-0.5, m+0.5]) + ... builder.add_flow(start=stabilizer, ms=[m]) + ... builder.add_flow(end=stabilizer, ms=[m]) + >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_name("LZ") + >>> builder.add_flow(start=obs, end=obs) + >>> chunk = builder.finish_chunk() + + >>> chunk.verify() + >>> print(chunk.to_closed_circuit()) + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0.5, 0) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1.5, 0) 3 + QUBIT_COORDS(2, 0) 4 + QUBIT_COORDS(2.5, 0) 5 + QUBIT_COORDS(3, 0) 6 + QUBIT_COORDS(3.5, 0) 7 + QUBIT_COORDS(4, 0) 8 + QUBIT_COORDS(4.5, 0) 9 + QUBIT_COORDS(5, 0) 10 + OBSERVABLE_INCLUDE(0) Z0 + TICK + MPP Z0*Z2 Z4*Z6 Z8*Z10 + TICK + MPP Z2*Z4 Z6*Z8 + TICK + R 9 7 5 3 1 + TICK + CX 8 9 6 7 4 5 2 3 0 1 + TICK + CX 8 7 6 5 4 3 2 1 10 9 + TICK + M 9 7 5 3 1 + DETECTOR(4.5, 0, 0) rec[-8] rec[-5] + DETECTOR(3.5, 0, 0) rec[-6] rec[-4] + DETECTOR(2.5, 0, 0) rec[-9] rec[-3] + DETECTOR(1.5, 0, 0) rec[-7] rec[-2] + DETECTOR(0.5, 0, 0) rec[-10] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + MPP Z0*Z2 Z4*Z6 Z8*Z10 + TICK + MPP Z2*Z4 Z6*Z8 + DETECTOR(0.5, 0, 0) rec[-6] rec[-5] + DETECTOR(2.5, 0, 0) rec[-8] rec[-4] + DETECTOR(4.5, 0, 0) rec[-10] rec[-3] + DETECTOR(1.5, 0, 0) rec[-7] rec[-2] + DETECTOR(3.5, 0, 0) rec[-9] rec[-1] + TICK + OBSERVABLE_INCLUDE(0) Z0 """ ``` @@ -724,6 +761,11 @@ def __init__( Args: allowed_qubits: Defaults to None (everything allowed). Specifies the qubit positions that the circuit is permitted to contain. + + >>> import stimflow as sf + >>> data_qubits = range(5) + >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] + >>> builder = sf.ChunkBuilder(allowed_qubits=[*data_qubits, *measure_qubits]) """ ``` @@ -1494,6 +1536,9 @@ def with_repetitions( class ChunkReflow: """An adapter chunk for attaching chunks describing the same thing in different ways. + (This class is still a work in progress; it is not simple to use and it + doesn't achieve all the desired functionality.) + For example, consider two surface code idle round chunks where one has the logical operator on the left side and the other has the logical operator on the right side. They can't be directly concatenated, because their flows don't match. But a reflow @@ -2144,310 +2189,6 @@ def without_empty_layers( """ ``` - -```python -# stimflow.LayerInteract - -# (at top-level in the stimflow module) -@dataclasses.dataclass -class LayerInteract: - """A layer of controlled Pauli gates (like CX, CZ, and XCY). - """ - targets1: list[int] - targets2: list[int] - bases1: list[str] - bases2: list[str] -``` - - -```python -# stimflow.LayerInteract.append_into_stim_circuit - -# (in class stimflow.LayerInteract) -def append_into_stim_circuit( - self, - out: stim.Circuit, -) -> None: -``` - - -```python -# stimflow.LayerInteract.copy - -# (in class stimflow.LayerInteract) -def copy( - self, -) -> LayerInteract: -``` - - -```python -# stimflow.LayerInteract.locally_optimized - -# (in class stimflow.LayerInteract) -def locally_optimized( - self, - next_layer: Layer | None, -) -> list[Layer | None]: -``` - - -```python -# stimflow.LayerInteract.rotate_to_z_layer - -# (in class stimflow.LayerInteract) -def rotate_to_z_layer( - self, -): -``` - - -```python -# stimflow.LayerInteract.to_z_basis - -# (in class stimflow.LayerInteract) -def to_z_basis( - self, -) -> list[Layer]: -``` - - -```python -# stimflow.LayerInteract.touched - -# (in class stimflow.LayerInteract) -def touched( - self, -) -> set[int]: -``` - - -```python -# stimflow.LayerMeasure - -# (at top-level in the stimflow module) -@dataclasses.dataclass -class LayerMeasure: - """A layer of single qubit Pauli basis measurement operations. - """ - targets: list[int] - bases: list[str] -``` - - -```python -# stimflow.LayerMeasure.append_into_stim_circuit - -# (in class stimflow.LayerMeasure) -def append_into_stim_circuit( - self, - out: stim.Circuit, -) -> None: -``` - - -```python -# stimflow.LayerMeasure.copy - -# (in class stimflow.LayerMeasure) -def copy( - self, -) -> LayerMeasure: -``` - - -```python -# stimflow.LayerMeasure.locally_optimized - -# (in class stimflow.LayerMeasure) -def locally_optimized( - self, - next_layer: Layer | None, -) -> list[Layer | None]: -``` - - -```python -# stimflow.LayerMeasure.to_z_basis - -# (in class stimflow.LayerMeasure) -def to_z_basis( - self, -) -> list[Layer]: -``` - - -```python -# stimflow.LayerMeasure.touched - -# (in class stimflow.LayerMeasure) -def touched( - self, -) -> set[int]: -``` - - -```python -# stimflow.LayerReset - -# (at top-level in the stimflow module) -@dataclasses.dataclass -class LayerReset: - """A layer of reset gates. - """ - targets: dict[int, Literal['X', 'Y', 'Z']] -``` - - -```python -# stimflow.LayerReset.append_into_stim_circuit - -# (in class stimflow.LayerReset) -def append_into_stim_circuit( - self, - out: stim.Circuit, -) -> None: -``` - - -```python -# stimflow.LayerReset.copy - -# (in class stimflow.LayerReset) -def copy( - self, -) -> LayerReset: -``` - - -```python -# stimflow.LayerReset.locally_optimized - -# (in class stimflow.LayerReset) -def locally_optimized( - self, - next_layer: Layer | None, -) -> list[Layer | None]: -``` - - -```python -# stimflow.LayerReset.to_z_basis - -# (in class stimflow.LayerReset) -def to_z_basis( - self, -) -> list[Layer]: -``` - - -```python -# stimflow.LayerReset.touched - -# (in class stimflow.LayerReset) -def touched( - self, -) -> set[int]: -``` - - -```python -# stimflow.LayerRotation - -# (at top-level in the stimflow module) -@dataclasses.dataclass -class LayerRotation: - """A layer of single qubit Clifford rotation gates. - """ - named_rotations: dict[int, str] -``` - - -```python -# stimflow.LayerRotation.append_into_stim_circuit - -# (in class stimflow.LayerRotation) -def append_into_stim_circuit( - self, - out: stim.Circuit, -) -> None: -``` - - -```python -# stimflow.LayerRotation.append_named_rotation - -# (in class stimflow.LayerRotation) -def append_named_rotation( - self, - name: str, - target: int, -): -``` - - -```python -# stimflow.LayerRotation.copy - -# (in class stimflow.LayerRotation) -def copy( - self, -) -> LayerRotation: -``` - - -```python -# stimflow.LayerRotation.inverse - -# (in class stimflow.LayerRotation) -def inverse( - self, -) -> LayerRotation: -``` - - -```python -# stimflow.LayerRotation.is_vacuous - -# (in class stimflow.LayerRotation) -def is_vacuous( - self, -) -> bool: -``` - - -```python -# stimflow.LayerRotation.locally_optimized - -# (in class stimflow.LayerRotation) -def locally_optimized( - self, - next_layer: Layer | None, -) -> list[Layer | None]: -``` - - -```python -# stimflow.LayerRotation.prepend_named_rotation - -# (in class stimflow.LayerRotation) -def prepend_named_rotation( - self, - name: str, - target: int, -): -``` - - -```python -# stimflow.LayerRotation.touched - -# (in class stimflow.LayerRotation) -def touched( - self, -) -> set[int]: -``` - ```python # stimflow.LineDataFor3DModel @@ -4056,16 +3797,6 @@ def append_reindexed_content_to_circuit( """ ``` - -```python -# stimflow.circuit_to_cycle_code_slices - -# (at top-level in the stimflow module) -def circuit_to_cycle_code_slices( - circuit: stim.Circuit, -) -> dict[int, StabilizerCode]: -``` - ```python # stimflow.circuit_to_dem_target_measurement_records_map @@ -4086,35 +3817,6 @@ def circuit_with_xz_flipped( ) -> stim.Circuit: ``` - -```python -# stimflow.compile_chunks_into_circuit - -# (at top-level in the stimflow module) -def compile_chunks_into_circuit( - chunks: list[Chunk | ChunkLoop | ChunkReflow], - *, - use_magic_time_boundaries: bool = False, - metadata_func: Callable[[Flow], FlowMetadata] = lambda _: FlowMetadata(), -) -> stim.Circuit: - """Stitches together a series of chunks into a fault tolerant circuit. - - Args: - chunks: The sequence of chunks to compile into a circuit. - use_magic_time_boundaries: Defaults to False. When False, an error will be raised if the - first chunk has any non-empty input flows or the last chunk has any non-empty output - flows (indicating the circuit is not complete). When True, the compiler will - automatically close those flows by inserting MPP and OBSERVABLE_INCLUDE instructions to - explain the dangling flows. - metadata_func: Defaults to using no metadata. This function should take a stimflow.Flow and - return a stimflow.FlowMetadata. The metadata is used for adding tags to coordinates to - DETECTOR instructions and tags to DETECTOR/OBSERVABLE_INCLUDE instructions. - - Returns: - The compiled circuit. - """ -``` - ```python # stimflow.count_measurement_layers diff --git a/glue/stimflow/src/stimflow/__init__.py b/glue/stimflow/src/stimflow/__init__.py index 39e533a4..8f15097a 100644 --- a/glue/stimflow/src/stimflow/__init__.py +++ b/glue/stimflow/src/stimflow/__init__.py @@ -7,8 +7,6 @@ ChunkInterface, ChunkLoop, ChunkReflow, - circuit_to_cycle_code_slices, - compile_chunks_into_circuit, find_d1_error, find_d2_error, FlowMetadata, @@ -39,11 +37,7 @@ xor_sorted, ) from stimflow._layers import ( - LayerInteract, LayerCircuit, - LayerMeasure, - LayerReset, - LayerRotation, transpile_to_z_basis_interaction_circuit, ) from stimflow._viz import ( diff --git a/glue/stimflow/src/stimflow/_chunk/__init__.py b/glue/stimflow/src/stimflow/_chunk/__init__.py index 5e27ff65..b42690d4 100644 --- a/glue/stimflow/src/stimflow/_chunk/__init__.py +++ b/glue/stimflow/src/stimflow/_chunk/__init__.py @@ -2,12 +2,11 @@ from stimflow._chunk._chunk import Chunk from stimflow._chunk._chunk_builder import ChunkBuilder -from stimflow._chunk._chunk_compiler import ChunkCompiler, compile_chunks_into_circuit +from stimflow._chunk._chunk_compiler import ChunkCompiler from stimflow._chunk._chunk_interface import ChunkInterface from stimflow._chunk._chunk_loop import ChunkLoop from stimflow._chunk._chunk_reflow import ChunkReflow from stimflow._chunk._code_util import ( - circuit_to_cycle_code_slices, find_d1_error, find_d2_error, transversal_code_transition_chunks, diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py index 1072c158..39a8f984 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py @@ -16,10 +16,77 @@ class ChunkBuilder: - """Helper class for building stim circuits. - - Handles qubit indexing (complex -> int). - Handles measurement tracking (naming results and referring to them by name). + """A helper class for building chunks. + + This class takes care of details like converting qubit coordinates into qubit indices, + storing and retrieving measurement indices, and accumulating flow data. + + Example: + >>> import stimflow as sf + + >>> # Build a repetition code idling chunk. + >>> d = 5 + >>> data_qubits = range(d) + >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] + >>> builder = sf.ChunkBuilder() + >>> builder.append("R", measure_qubits) + >>> builder.append("TICK") + >>> builder.append("CX", [(m-0.5, m) for m in measure_qubits]) + >>> builder.append("TICK") + >>> builder.append("CX", [(m+0.5, m) for m in measure_qubits]) + >>> builder.append("TICK") + >>> builder.append("M", measure_qubits) + >>> for m in measure_qubits: + ... stabilizer = sf.PauliMap.from_zs([m-0.5, m+0.5]) + ... builder.add_flow(start=stabilizer, ms=[m]) + ... builder.add_flow(end=stabilizer, ms=[m]) + >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_name("LZ") + >>> builder.add_flow(start=obs, end=obs) + >>> chunk = builder.finish_chunk() + + >>> chunk.verify() + >>> print(chunk.to_closed_circuit()) + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0.5, 0) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1.5, 0) 3 + QUBIT_COORDS(2, 0) 4 + QUBIT_COORDS(2.5, 0) 5 + QUBIT_COORDS(3, 0) 6 + QUBIT_COORDS(3.5, 0) 7 + QUBIT_COORDS(4, 0) 8 + QUBIT_COORDS(4.5, 0) 9 + QUBIT_COORDS(5, 0) 10 + OBSERVABLE_INCLUDE(0) Z0 + TICK + MPP Z0*Z2 Z4*Z6 Z8*Z10 + TICK + MPP Z2*Z4 Z6*Z8 + TICK + R 9 7 5 3 1 + TICK + CX 8 9 6 7 4 5 2 3 0 1 + TICK + CX 8 7 6 5 4 3 2 1 10 9 + TICK + M 9 7 5 3 1 + DETECTOR(4.5, 0, 0) rec[-8] rec[-5] + DETECTOR(3.5, 0, 0) rec[-6] rec[-4] + DETECTOR(2.5, 0, 0) rec[-9] rec[-3] + DETECTOR(1.5, 0, 0) rec[-7] rec[-2] + DETECTOR(0.5, 0, 0) rec[-10] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + MPP Z0*Z2 Z4*Z6 Z8*Z10 + TICK + MPP Z2*Z4 Z6*Z8 + DETECTOR(0.5, 0, 0) rec[-6] rec[-5] + DETECTOR(2.5, 0, 0) rec[-8] rec[-4] + DETECTOR(4.5, 0, 0) rec[-10] rec[-3] + DETECTOR(1.5, 0, 0) rec[-7] rec[-2] + DETECTOR(3.5, 0, 0) rec[-9] rec[-1] + TICK + OBSERVABLE_INCLUDE(0) Z0 """ def __init__( @@ -31,6 +98,11 @@ def __init__( Args: allowed_qubits: Defaults to None (everything allowed). Specifies the qubit positions that the circuit is permitted to contain. + + >>> import stimflow as sf + >>> data_qubits = range(5) + >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] + >>> builder = sf.ChunkBuilder(allowed_qubits=[*data_qubits, *measure_qubits]) """ self.allowed_qubits: set[complex] | None = None if allowed_qubits is None else set(allowed_qubits) self._num_measurements: int = 0 diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py index 7a24b639..8befc07a 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py @@ -615,42 +615,3 @@ def _compute_attached_flows_and_discards( raise ValueError("\n".join(lines)) return result, outgoing_discards - - -def compile_chunks_into_circuit( - chunks: list[Chunk | ChunkLoop | ChunkReflow], - *, - use_magic_time_boundaries: bool = False, - metadata_func: Callable[[Flow], FlowMetadata] = lambda _: FlowMetadata(), -) -> stim.Circuit: - """Stitches together a series of chunks into a fault tolerant circuit. - - Args: - chunks: The sequence of chunks to compile into a circuit. - use_magic_time_boundaries: Defaults to False. When False, an error will be raised if the - first chunk has any non-empty input flows or the last chunk has any non-empty output - flows (indicating the circuit is not complete). When True, the compiler will - automatically close those flows by inserting MPP and OBSERVABLE_INCLUDE instructions to - explain the dangling flows. - metadata_func: Defaults to using no metadata. This function should take a stimflow.Flow and - return a stimflow.FlowMetadata. The metadata is used for adding tags to coordinates to - DETECTOR instructions and tags to DETECTOR/OBSERVABLE_INCLUDE instructions. - - Returns: - The compiled circuit. - """ - compiler = ChunkCompiler(metadata_func=metadata_func) - - if use_magic_time_boundaries and chunks: - compiler.append_magic_init_chunk() - - for k, chunk in enumerate(chunks): - try: - compiler.append(chunk) - except (ValueError, NotImplementedError) as ex: - raise type(ex)(f"Encountered error while appending chunk index {k}:\n{ex}") from ex - - if use_magic_time_boundaries and chunks: - compiler.append_magic_end_chunk() - - return compiler.finish_circuit() diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py index 7c5c5bae..f4b0ef8c 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py @@ -335,7 +335,11 @@ def test_compile_postselected_chunks(): flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), mids=[0])], ) - assert stimflow.compile_chunks_into_circuit([chunk1, chunk2, chunk3]).flattened() == stim.Circuit( + compiler = stimflow.ChunkCompiler() + compiler.append(chunk1) + compiler.append(chunk2) + compiler.append(chunk3) + assert compiler.finish_circuit().flattened() == stim.Circuit( """ QUBIT_COORDS(0, 0) 0 R 0 @@ -348,16 +352,13 @@ def test_compile_postselected_chunks(): """ ) - assert stimflow.compile_chunks_into_circuit( - [ - chunk1.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk1.flows]), - chunk2, - chunk3, - ], - metadata_func=lambda flow: stimflow.FlowMetadata( - extra_coords=[999] if "postselect" in flow.flags else [] - ), - ).flattened() == stim.Circuit( + compiler = stimflow.ChunkCompiler(metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + )) + compiler.append(chunk1.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk1.flows])) + compiler.append(chunk2) + compiler.append(chunk3) + assert compiler.finish_circuit().flattened() == stim.Circuit( """ QUBIT_COORDS(0, 0) 0 R 0 @@ -370,16 +371,13 @@ def test_compile_postselected_chunks(): """ ) - assert stimflow.compile_chunks_into_circuit( - [ - chunk1, - chunk2.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk2.flows]), - chunk3, - ], - metadata_func=lambda flow: stimflow.FlowMetadata( - extra_coords=[999] if "postselect" in flow.flags else [] - ), - ).flattened() == stim.Circuit( + compiler = stimflow.ChunkCompiler(metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + )) + compiler.append(chunk1) + compiler.append(chunk2.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk2.flows])) + compiler.append(chunk3) + assert compiler.finish_circuit().flattened() == stim.Circuit( """ QUBIT_COORDS(0, 0) 0 R 0 @@ -392,16 +390,13 @@ def test_compile_postselected_chunks(): """ ) - assert stimflow.compile_chunks_into_circuit( - [ - chunk1, - chunk2, - chunk3.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk3.flows]), - ], - metadata_func=lambda flow: stimflow.FlowMetadata( - extra_coords=[999] if "postselect" in flow.flags else [] - ), - ).flattened() == stim.Circuit( + compiler = stimflow.ChunkCompiler(metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + )) + compiler.append(chunk1) + compiler.append(chunk2) + compiler.append(chunk3.with_edits(flows=[f.with_edits(flags={"postselect"}) for f in chunk3.flows])) + assert compiler.finish_circuit().flattened() == stim.Circuit( """ QUBIT_COORDS(0, 0) 0 R 0 @@ -414,18 +409,15 @@ def test_compile_postselected_chunks(): """ ) - assert stimflow.compile_chunks_into_circuit( - [ - chunk1, - chunk2.with_edits( - flows=[f.with_edits(flags={"postselect"}) if f.start else f for f in chunk2.flows] - ), - chunk3, - ], - metadata_func=lambda flow: stimflow.FlowMetadata( - extra_coords=[999] if "postselect" in flow.flags else [] - ), - ).flattened() == stim.Circuit( + compiler = stimflow.ChunkCompiler(metadata_func=lambda flow: stimflow.FlowMetadata( + extra_coords=[999] if "postselect" in flow.flags else [] + )) + compiler.append(chunk1) + compiler.append(chunk2.with_edits( + flows=[f.with_edits(flags={"postselect"}) if f.start else f for f in chunk2.flows] + )) + compiler.append(chunk3) + assert compiler.finish_circuit().flattened() == stim.Circuit( """ QUBIT_COORDS(0, 0) 0 R 0 diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py index 08550528..c6d9dd34 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py @@ -16,6 +16,9 @@ class ChunkReflow: """An adapter chunk for attaching chunks describing the same thing in different ways. + (This class is still a work in progress; it is not simple to use and it + doesn't achieve all the desired functionality.) + For example, consider two surface code idle round chunks where one has the logical operator on the left side and the other has the logical operator on the right side. They can't be directly concatenated, because their flows don't match. But a reflow diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py index c8ab89e4..f2792850 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py @@ -83,7 +83,11 @@ def test_reflow(): chunk1.verify() chunk2.verify() reflow.verify() - stimflow.compile_chunks_into_circuit([chunk1, reflow, chunk2]) + compiler = stimflow.ChunkCompiler() + compiler.append(chunk1) + compiler.append(reflow) + compiler.append(chunk2) + assert compiler.finish_circuit() is not None def test_from_circuit_with_mpp_boundaries_simple(): diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util.py b/glue/stimflow/src/stimflow/_chunk/_code_util.py index a58395ee..be2e8f41 100644 --- a/glue/stimflow/src/stimflow/_chunk/_code_util.py +++ b/glue/stimflow/src/stimflow/_chunk/_code_util.py @@ -14,46 +14,6 @@ from stimflow._chunk._chunk_reflow import ChunkReflow -def circuit_to_cycle_code_slices(circuit: stim.Circuit) -> dict[int, StabilizerCode]: - from stimflow._chunk._patch import Patch - from stimflow._chunk._stabilizer_code import StabilizerCode - - t = 0 - ticks = set() - for inst in circuit.flattened(): - if inst.name == "TICK": - t += 1 - elif inst.name in ["R", "RX"]: - if t - 1 not in ticks and t - 2 not in ticks: - ticks.add(max(t - 1, 0)) - elif inst.name in ["M", "MX"]: - ticks.add(t) - - regions = circuit.detecting_regions(ticks=ticks) - layers: dict[int, list[tuple[stim.DemTarget, stim.PauliString]]] = collections.defaultdict(list) - for dem_target, tick2paulis in regions.items(): - for tick, pauli_string in tick2paulis.items(): - layers[tick].append((dem_target, pauli_string)) - - i2q = {k: r + i * 1j for k, (r, i) in circuit.get_final_qubit_coordinates().items()} - - codes = {} - for tick, layer in sorted(layers.items()): - obs = [] - tiles = [] - for dem_target, pauli_string in layer: - pauli_map = PauliMap( - {i2q[q]: "_XYZ"[pauli_string[q]] for q in pauli_string.pauli_indices()} - ) - if dem_target.is_relative_detector_id(): - tiles.append(pauli_map.to_tile().with_edits(flags={str(dem_target.val)})) - else: - obs.append(pauli_map) - codes[tick] = StabilizerCode(stabilizers=Patch(tiles), logicals=obs) - - return codes - - def find_d1_error( obj: stim.Circuit | stim.DetectorErrorModel, ) -> stim.ExplainedError | stim.DemInstruction | None: diff --git a/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py b/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py index 001df212..27d88f2e 100644 --- a/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py +++ b/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py @@ -2,18 +2,18 @@ import stim -import stimflow +from stimflow._layers._layer_rotation import LayerRotation def test_fuses_rotations(): - layer = stimflow.LayerRotation() + layer = LayerRotation() layer.append_named_rotation("H", 0) layer.append_named_rotation("H_NXZ", 0) assert layer.named_rotations[0] == "Y" def test_output(): - layer = stimflow.LayerRotation() + layer = LayerRotation() layer.append_named_rotation("H", 0) layer.append_named_rotation("C_XYZ", 0) From d20e630ed268bd9617e9eeed3d60bf74bac3cf64 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 19:00:56 -0700 Subject: [PATCH 7/9] fused_with_next_flow, remove redundant obs_key arguments, mids -> measurement_indices --- glue/stimflow/doc/api.md | 37 +++----- glue/stimflow/src/stimflow/_chunk/_chunk.py | 31 ++++--- .../src/stimflow/_chunk/_chunk_builder.py | 20 ++-- .../src/stimflow/_chunk/_chunk_compiler.py | 19 ++-- .../stimflow/_chunk/_chunk_compiler_test.py | 66 +++++++------ .../src/stimflow/_chunk/_chunk_test.py | 53 +++++------ .../src/stimflow/_chunk/_flow_util_test.py | 14 ++- glue/stimflow/src/stimflow/_core/_flow.py | 92 ++++++++----------- 8 files changed, 156 insertions(+), 176 deletions(-) diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 2c3d25af..948a6992 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -91,7 +91,7 @@ - [`stimflow.Flow`](#stimflow.Flow) - [`stimflow.Flow.__init__`](#stimflow.Flow.__init__) - [`stimflow.Flow.__mul__`](#stimflow.Flow.__mul__) - - [`stimflow.Flow.fuse_with_next_flow`](#stimflow.Flow.fuse_with_next_flow) + - [`stimflow.Flow.fused_with_next_flow`](#stimflow.Flow.fused_with_next_flow) - [`stimflow.Flow.obs_key`](#stimflow.Flow.obs_key) - [`stimflow.Flow.to_stim_flow`](#stimflow.Flow.to_stim_flow) - [`stimflow.Flow.with_edits`](#stimflow.Flow.with_edits) @@ -582,14 +582,14 @@ def verify_distance_is_at_least( ... '''), ... flows=[ ... sf.Flow(start=lz, end=lz), - ... sf.Flow(start=zz01, mids=[0]), - ... sf.Flow(start=zz12, mids=[1]), - ... sf.Flow(start=zz23, mids=[2]), - ... sf.Flow(start=zz34, mids=[3]), - ... sf.Flow(end=zz01, mids=[0]), - ... sf.Flow(end=zz12, mids=[1]), - ... sf.Flow(end=zz23, mids=[2]), - ... sf.Flow(end=zz34, mids=[3]), + ... sf.Flow(start=zz01, measurement_indices=[0]), + ... sf.Flow(start=zz12, measurement_indices=[1]), + ... sf.Flow(start=zz23, measurement_indices=[2]), + ... sf.Flow(start=zz34, measurement_indices=[3]), + ... sf.Flow(end=zz01, measurement_indices=[0]), + ... sf.Flow(end=zz12, measurement_indices=[1]), + ... sf.Flow(end=zz23, measurement_indices=[2]), + ... sf.Flow(end=zz34, measurement_indices=[3]), ... ], ... ) >>> chunk.verify_distance_is_at_least(3) @@ -803,7 +803,6 @@ def add_flow( end: "PauliMap | Tile | Literal['auto'] | None" = None, ms: "Iterable[Any] | Literal['auto']" = (), ignore_unmatched_ms: bool = False, - obs_key: Any = None, center: complex | None = None, flags: Iterable[str] = frozenset(), sign: bool | None = None, @@ -826,8 +825,6 @@ def add_flow( ignore_unmatched_ms: Defaults to False. When set to False, unrecognized measurement ids cause the method to raise an exception instead of adding the flow. When set to True, unrecognized measurements are silently discarded. - obs_key: Defaults to None (not a logical operator). If this is set to a value other - than None, it identifies the logical operator whose flow the flow is describing. center: Defaults to None (unused). Optional metadata specifying coordinates for the flow. Typically these coordinates will end up being exposed as the parens args on the DETECTOR instruction created when producing a stim circuit. When not @@ -1709,8 +1706,7 @@ def __init__( *, start: PauliMap | Tile | None = None, end: PauliMap | Tile | None = None, - mids: Iterable[int] = (), - obs_key: Any = None, + measurement_indices: Iterable[int] = (), center: complex | None = None, flags: Iterable[Any] = frozenset(), sign: bool | None = None, @@ -1722,12 +1718,10 @@ def __init__( circuit (before *all* operations, including resets). end: Defaults to None (empty). The Pauli product operator at the end of the circuit (after *all* operations, including measurements). - mids: Defaults to empty. Indices of measurements that mediate the flow (that multiply + measurement_indices: Defaults to empty. Indices of measurements that mediate the flow (that multiply into it as it traverses the circuit). center: Defaults to None (unspecified). Specifies a 2d coordinate to use in metadata when the flow is completed into a detector. Incompatible with obs_key. - obs_key: Defaults to None (detector flow). Identifies that this is an observable flow - (instead of a detector flow) and gives a name that be used when linking chunks. flags: Defaults to empty. Custom information about the flow, that can be used by code operating on chunks for a variety of purposes. For example, this could identify the "color" of the flow in a color code. @@ -1750,12 +1744,12 @@ def __mul__( """ ``` - + ```python -# stimflow.Flow.fuse_with_next_flow +# stimflow.Flow.fused_with_next_flow # (in class stimflow.Flow) -def fuse_with_next_flow( +def fused_with_next_flow( self, next_flow: Flow, *, @@ -1798,8 +1792,7 @@ def with_edits( start: PauliMap = , end: PauliMap = , measurement_indices: Iterable[int] = , - obs_key: Any = , - center: complex = , + center: complex | None = , flags: Iterable[str] = , sign: Any = , ) -> Flow: diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk.py b/glue/stimflow/src/stimflow/_chunk/_chunk.py index b6cdbe67..44c30bc3 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk.py @@ -379,15 +379,19 @@ def from_circuit_with_mpp_boundaries(circuit: stim.Circuit) -> Chunk: start_pm = PauliMap(start_ps).with_transformed_coords(lambda i: i2q[i]) end_pm = PauliMap(end_ps).with_transformed_coords(lambda i: i2q[i]) + if target.is_logical_observable_id(): + start_pm = start_pm.with_name(target.val) + end_pm = end_pm.with_name(target.val) + center = sum(start_pm.keys()) + sum(end_pm.keys()) + if center: + center /= len(start_pm) + len(end_pm) flows.append( Flow( start=start_pm, end=end_pm, - mids=[m - record_range.start for m in records[target] if m in record_range], - obs_key=None if target.is_relative_detector_id() else target.val, - center=(sum(start_pm.keys()) + sum(end_pm.keys())) - / (len(start_pm) + len(end_pm)), + measurement_indices=[m - record_range.start for m in records[target] if m in record_range], + center=center, ) ) @@ -591,14 +595,14 @@ def verify_distance_is_at_least(self, minimum_distance: int, *, noise: float | N ... '''), ... flows=[ ... sf.Flow(start=lz, end=lz), - ... sf.Flow(start=zz01, mids=[0]), - ... sf.Flow(start=zz12, mids=[1]), - ... sf.Flow(start=zz23, mids=[2]), - ... sf.Flow(start=zz34, mids=[3]), - ... sf.Flow(end=zz01, mids=[0]), - ... sf.Flow(end=zz12, mids=[1]), - ... sf.Flow(end=zz23, mids=[2]), - ... sf.Flow(end=zz34, mids=[3]), + ... sf.Flow(start=zz01, measurement_indices=[0]), + ... sf.Flow(start=zz12, measurement_indices=[1]), + ... sf.Flow(start=zz23, measurement_indices=[2]), + ... sf.Flow(start=zz34, measurement_indices=[3]), + ... sf.Flow(end=zz01, measurement_indices=[0]), + ... sf.Flow(end=zz12, measurement_indices=[1]), + ... sf.Flow(end=zz23, measurement_indices=[2]), + ... sf.Flow(end=zz34, measurement_indices=[3]), ... ], ... ) >>> chunk.verify_distance_is_at_least(3) @@ -802,9 +806,8 @@ def time_reversed(self) -> Chunk: center=flow.center, start=flow.end, end=flow.start, - mids=[m + nm for m in rev_flow.measurements_copy()], + measurement_indices=[m + nm for m in rev_flow.measurements_copy()], flags=flow.flags, - obs_key=flow.obs_key, ) for flow, rev_flow in zip(self.flows, rev_flows, strict=True) ], diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py index 39a8f984..dea3c865 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py @@ -248,7 +248,6 @@ def add_flow( end: PauliMap | Tile | Literal["auto"] | None = None, ms: Iterable[Any] | Literal["auto"] = (), ignore_unmatched_ms: bool = False, - obs_key: Any = None, center: complex | None = None, flags: Iterable[str] = frozenset(), sign: bool | None = None, @@ -271,8 +270,6 @@ def add_flow( ignore_unmatched_ms: Defaults to False. When set to False, unrecognized measurement ids cause the method to raise an exception instead of adding the flow. When set to True, unrecognized measurements are silently discarded. - obs_key: Defaults to None (not a logical operator). If this is set to a value other - than None, it identifies the logical operator whose flow the flow is describing. center: Defaults to None (unused). Optional metadata specifying coordinates for the flow. Typically these coordinates will end up being exposed as the parens args on the DETECTOR instruction created when producing a stim circuit. When not @@ -305,23 +302,28 @@ def add_flow( f" {start=}" f" {ms=}" f" {end=}") + if isinstance(start, PauliMap): + obs_key = start.name + elif isinstance(end, PauliMap): + obs_key = end.name + else: + obs_key = None out = self._flows if start == "auto": out = self._flows_with_auto_start - start = None + start = PauliMap(name=obs_key) elif end == "auto": out = self._flows_with_auto_end - end = None + end = PauliMap(name=obs_key) elif ms == "auto": out = self._flows_with_auto_ms ms = () out.append( Flow( - start=cast(PauliMap, start), - end=cast(PauliMap, end), - mids=self.lookup_mids(ms, ignore_unmatched=ignore_unmatched_ms), - obs_key=obs_key, + start=start, + end=end, + measurement_indices=self.lookup_mids(ms, ignore_unmatched=ignore_unmatched_ms), center=center, flags=flags, sign=sign, diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py index 8befc07a..0fb2d530 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py @@ -169,7 +169,7 @@ def append_magic_init_chunk(self, expected: ChunkInterface | None = None) -> Non targets = [stim.target_pauli(self.q2i[q], p) for q, p in obs_port.items()] self.open_flows[obs_port] = Flow(end=obs_port) obs_index = self.o2i.setdefault(obs_port.name, len(self.o2i)) - metadata = self.metadata_func(Flow(obs_key=obs_port.name)) + metadata = self.metadata_func(Flow(start=PauliMap(name=obs_port.name))) self.circuit.append("OBSERVABLE_INCLUDE", targets, arg=obs_index, tag=metadata.tag) self.circuit.append("TICK") @@ -177,7 +177,7 @@ def append_magic_init_chunk(self, expected: ChunkInterface | None = None) -> Non for port in layer: assert port not in self.open_flows targets = [stim.target_pauli(self.q2i[q], p) for q, p in port.items()] - self.open_flows[port] = Flow(end=port, mids=[self.num_measurements]) + self.open_flows[port] = Flow(end=port, measurement_indices=[self.num_measurements]) self.circuit.append("MPP", stim.target_combined_paulis(targets)) self.num_measurements += 1 self.circuit.append("TICK") @@ -215,8 +215,8 @@ def append_magic_end_chunk(self, expected: ChunkInterface | None = None) -> None targets = [stim.target_pauli(self.q2i[q], p) for q, p in port.items()] flow = self.open_flows.pop(port) assert flow != "discard" - flow = cast(Flow, flow).fuse_with_next_flow( - Flow(start=port, mids=[self.num_measurements]), next_flow_measure_offset=0 + flow = cast(Flow, flow).fused_with_next_flow( + Flow(start=port, measurement_indices=[self.num_measurements]), next_flow_measure_offset=0 ) self.circuit.append("MPP", stim.target_combined_paulis(targets)) self.num_measurements += 1 @@ -227,11 +227,11 @@ def append_magic_end_chunk(self, expected: ChunkInterface | None = None) -> None targets = [stim.target_pauli(self.q2i[q], p) for q, p in obs_port.items()] flow = self.open_flows.pop(obs_port) assert flow != "discard" - flow = cast(Flow, flow).fuse_with_next_flow( + flow = flow.fused_with_next_flow( Flow(start=obs_port), next_flow_measure_offset=0 ) obs_index = self.o2i.setdefault(obs_port.name, len(self.o2i)) - metadata = self.metadata_func(Flow(obs_key=obs_port.name)) + metadata = self.metadata_func(Flow(start=PauliMap(name=obs_port.name))) self.circuit.append("OBSERVABLE_INCLUDE", targets, arg=obs_index, tag=metadata.tag) completed_flows.append(flow) self._append_detectors(completed_flows=completed_flows) @@ -304,7 +304,7 @@ def _append_chunk_reflow(self, *, chunk_reflow: ChunkReflow) -> None: else Flow( start=None, end=output, - mids=tuple(sorted(measurements)), + measurement_indices=tuple(sorted(measurements)), flags=flags, center=sum(centers) / len(centers) if centers else None, ) @@ -507,7 +507,8 @@ def _append_detectors(self, *, completed_flows: list[Flow]): if flow.obs_key is None: dt = detector_pos_usage_counts[flow.center] detector_pos_usage_counts[flow.center] += 1 - coords = (flow.center.real, flow.center.imag, dt, *metadata.extra_coords) + coord = flow.center if flow.center is not None else -1 + coords = (coord.real, coord.imag, dt, *metadata.extra_coords) inserted_ops.append("DETECTOR", rec_targets, coords, tag=metadata.tag) else: obs_index = self.o2i.setdefault(flow.obs_key, len(self.o2i)) @@ -581,7 +582,7 @@ def _compute_attached_flows_and_discards( elif isinstance(prev, Flow): # Matched! Fuse them together. result.append( - prev.fuse_with_next_flow( + prev.fused_with_next_flow( new_flow, next_flow_measure_offset=self.num_measurements ) ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py index f4b0ef8c..14b513ed 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py @@ -73,7 +73,7 @@ def test_chunk_compiler_single_flow(): M 0 """ ), - flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([1 + 2j]), mids=[0], center=3 + 5j)], + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([1 + 2j]), measurement_indices=[0], center=3 + 5j)], ) ) assert compiler.finish_circuit() == stim.Circuit( @@ -97,7 +97,7 @@ def test_chunk_compiler_obs_flow_eager_dump(): R 0 """ ), - flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]), center=0, obs_key=0)], + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]).with_name(0), center=0)], ) ) compiler.append( @@ -110,11 +110,10 @@ def test_chunk_compiler_obs_flow_eager_dump(): ), flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_zs([0]), - end=stimflow.PauliMap.from_zs([0]), - mids=[0], + start=stimflow.PauliMap.from_zs([0]).with_name(0), + end=stimflow.PauliMap.from_zs([0]).with_name(0), + measurement_indices=[0], center=0, - obs_key=0, ) ], ) @@ -127,7 +126,7 @@ def test_chunk_compiler_obs_flow_eager_dump(): M 0 """ ), - flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), mids=[0], center=0, obs_key=0)], + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_name(0), measurement_indices=[0], center=0)], ) ) assert compiler.finish_circuit() == stim.Circuit( @@ -177,8 +176,8 @@ def test_chunk_compiler_loop(): """ ), flows=[ - stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), mids=[0], center=0), - stimflow.Flow(end=stimflow.PauliMap.from_zs([3]), mids=[0], center=0), + stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), measurement_indices=[0], center=0), + stimflow.Flow(end=stimflow.PauliMap.from_zs([3]), measurement_indices=[0], center=0), stimflow.Flow( start=stimflow.PauliMap.from_zs([1]), end=stimflow.PauliMap.from_zs([0]), center=0 ), @@ -205,7 +204,7 @@ def test_chunk_compiler_loop(): M 0 1 2 3 """ ), - flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([k]), mids=[k], center=0) for k in range(4)], + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([k]), measurement_indices=[k], center=0) for k in range(4)], ) ) assert compiler.finish_circuit() == stim.Circuit( @@ -249,7 +248,7 @@ def test_chunk_compiler_loop_obs(): R 0 """ ), - flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]), center=0, obs_key=3)], + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]).with_name(3), center=0)], ) ) compiler.append( @@ -264,11 +263,10 @@ def test_chunk_compiler_loop_obs(): ), flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_zs([0]), - end=stimflow.PauliMap.from_zs([0]), - mids=[0], + start=stimflow.PauliMap.from_zs([0]).with_name(3), + end=stimflow.PauliMap.from_zs([0]).with_name(3), + measurement_indices=[0], center=0, - obs_key=3, ) ], ) @@ -284,7 +282,7 @@ def test_chunk_compiler_loop_obs(): M 0 """ ), - flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), mids=[0], center=0, obs_key=3)], + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_name(3), measurement_indices=[0], center=0)], ) ) assert compiler.finish_circuit() == stim.Circuit( @@ -321,8 +319,8 @@ def test_compile_postselected_chunks(): ), q2i={0: 0}, flows=[ - stimflow.Flow(center=0, end=stimflow.PauliMap({0: "Z"}), mids=[0]), - stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), mids=[0]), + stimflow.Flow(center=0, end=stimflow.PauliMap({0: "Z"}), measurement_indices=[0]), + stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), measurement_indices=[0]), ], ) chunk3 = stimflow.Chunk( @@ -332,7 +330,7 @@ def test_compile_postselected_chunks(): """ ), q2i={0: 0}, - flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), mids=[0])], + flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), measurement_indices=[0])], ) compiler = stimflow.ChunkCompiler() @@ -456,8 +454,8 @@ def test_chunk_compiler_propagate_discards(): ), q2i={0: 0, 1: 1}, flows=[ - stimflow.Flow(start=zz, center=0, mids=[0]), - stimflow.Flow(end=zz, center=0, mids=[0]), + stimflow.Flow(start=zz, center=0, measurement_indices=[0]), + stimflow.Flow(end=zz, center=0, measurement_indices=[0]), stimflow.Flow(start=xx, end=xx, center=0), ], ) @@ -471,7 +469,7 @@ def test_chunk_compiler_propagate_discards(): ), q2i={0: 0, 1: 1}, discarded_inputs=[zz], - flows=[stimflow.Flow(start=xx, center=0, mids=[0])], + flows=[stimflow.Flow(start=xx, center=0, measurement_indices=[0])], ) ) assert c.finish_circuit() == stim.Circuit( @@ -503,8 +501,8 @@ def test_drop_observable_later(): ), q2i={0: 0, 1: 1}, flows=[ - stimflow.Flow(end=zz, obs_key="a", mids=[1]), - stimflow.Flow(end=xx, obs_key="b", mids=[0]), + stimflow.Flow(end=zz.with_name("a"), measurement_indices=[1]), + stimflow.Flow(end=xx.with_name("b"), measurement_indices=[0]), ], ) ) @@ -518,7 +516,7 @@ def test_drop_observable_later(): ), q2i={0: 0, 1: 1}, discarded_inputs=[zz.with_name("a")], - flows=[stimflow.Flow(start=xx, obs_key="b", mids=[0])], + flows=[stimflow.Flow(start=xx.with_name("b"), measurement_indices=[0])], ) ) @@ -547,9 +545,9 @@ def test_chunk_negative_index_flow_measurement(): """ ), flows=[ - stimflow.Flow(mids=[-1], center=0), - stimflow.Flow(mids=[-2], center=0), - stimflow.Flow(mids=[-3], center=0), + stimflow.Flow(measurement_indices=[-1], center=0), + stimflow.Flow(measurement_indices=[-2], center=0), + stimflow.Flow(measurement_indices=[-3], center=0), ], ) compiler = stimflow.ChunkCompiler() @@ -595,8 +593,8 @@ def test_merge_ticks(): """ ), flows=[ - stimflow.Flow(start=stimflow.PauliMap.from_zs([0, 2]), mids=[0]), - stimflow.Flow(end=stimflow.PauliMap.from_zs([0, 2]), mids=[-1]), + stimflow.Flow(start=stimflow.PauliMap.from_zs([0, 2]), measurement_indices=[0]), + stimflow.Flow(end=stimflow.PauliMap.from_zs([0, 2]), measurement_indices=[-1]), ], ) measure_chunk = init_chunk.time_reversed() @@ -684,7 +682,7 @@ def test_merges_with_loop(): ), flows=[ stimflow.Flow(start=stimflow.PauliMap.from_zs([0]), end=stimflow.PauliMap.from_zs([0])), - stimflow.Flow(mids=[0]), + stimflow.Flow(measurement_indices=[0]), ], ) * 5 @@ -696,19 +694,19 @@ def test_merges_with_loop(): QUBIT_COORDS(0, 1) 1 R 0 1 M 1 - DETECTOR(0, 0, 0) rec[-1] + DETECTOR(-1, 0, 0) rec[-1] SHIFT_COORDS(0, 0, 1) TICK REPEAT 3 { R 1 M 1 - DETECTOR(0, 0, 0) rec[-1] + DETECTOR(-1, 0, 0) rec[-1] SHIFT_COORDS(0, 0, 1) TICK } R 1 M 1 - DETECTOR(0, 0, 0) rec[-1] + DETECTOR(-1, 0, 0) rec[-1] SHIFT_COORDS(0, 0, 1) M 0 DETECTOR(0, 0, 0) rec[-1] diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py index f2792850..00eaf446 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py @@ -13,7 +13,7 @@ def test_inverse_flows(): """ ), q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, - flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({}), mids=[0], end=stimflow.PauliMap({1: "Z"}))], + flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({}), measurement_indices=[0], end=stimflow.PauliMap({1: "Z"}))], ) inverted = chunk.time_reversed() @@ -67,7 +67,7 @@ def test_reflow(): MPP Z0*Z1 """ ), - flows=[stimflow.Flow(end=xx, mids=[0], center=0), stimflow.Flow(end=zz, mids=[1], center=0)], + flows=[stimflow.Flow(end=xx, measurement_indices=[0], center=0), stimflow.Flow(end=zz, measurement_indices=[1], center=0)], ) chunk2 = stimflow.Chunk( q2i={0: 0, 1: 1}, @@ -76,7 +76,7 @@ def test_reflow(): MPP Y0*Y1 """ ), - flows=[stimflow.Flow(start=yy, mids=[0], center=0)], + flows=[stimflow.Flow(start=yy, measurement_indices=[0], center=0)], discarded_inputs=[xx], ) reflow = stimflow.ChunkReflow({yy: [xx, zz], xx: [xx]}) @@ -109,8 +109,7 @@ def test_from_circuit_with_mpp_boundaries_simple(): stimflow.Flow( start=stimflow.PauliMap.from_xs([1 + 2j]), end=stimflow.PauliMap.from_zs([1 + 2j]), - mids=(), - obs_key=None, + measurement_indices=(), center=1 + 2j, ) ], @@ -168,8 +167,7 @@ def test_from_circuit_with_mpp_boundaries_simple(): stimflow.Flow( start=stimflow.PauliMap.from_xs([1 + 2j]), end=stimflow.PauliMap.from_zs([1 + 2j]), - mids=(0,), - obs_key=None, + measurement_indices=(0,), center=1 + 2j, ) ], @@ -207,10 +205,9 @@ def test_from_circuit_with_mpp_boundaries_simple(): q2i={1 + 2j: 0, 1 + 3j: 1}, flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_xs([1 + 2j]), - end=stimflow.PauliMap.from_zs([1 + 2j]), - mids=(0,), - obs_key=0, + start=stimflow.PauliMap.from_xs([1 + 2j]).with_name(0), + end=stimflow.PauliMap.from_zs([1 + 2j]).with_name(0), + measurement_indices=(0,), center=1 + 2j, ) ], @@ -468,7 +465,7 @@ def test_chunk_viewer(): """ ), q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, - flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({}), mids=[0], end=stimflow.PauliMap({1: "Z"}))], + flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({}), measurement_indices=[0], end=stimflow.PauliMap({1: "Z"}))], ) assert chunk.to_html_viewer() is not None @@ -487,17 +484,17 @@ def test_anticommuting_obs_flows(): """ ), flows=[ - stimflow.Flow(start=stimflow.PauliMap({"X": [0, 1, 1j, 1 + 1j]}), mids=[0]), - stimflow.Flow(end=stimflow.PauliMap({"X": [0, 1, 1j, 1 + 1j]}), mids=[0]), - stimflow.Flow(start=stimflow.PauliMap({"Z": [0, 1]}), mids=[1]), - stimflow.Flow(end=stimflow.PauliMap({"Z": [0, 1]}), mids=[1]), - stimflow.Flow(start=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), mids=[2]), - stimflow.Flow(end=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), mids=[2]), + stimflow.Flow(start=stimflow.PauliMap({"X": [0, 1, 1j, 1 + 1j]}), measurement_indices=[0]), + stimflow.Flow(end=stimflow.PauliMap({"X": [0, 1, 1j, 1 + 1j]}), measurement_indices=[0]), + stimflow.Flow(start=stimflow.PauliMap({"Z": [0, 1]}), measurement_indices=[1]), + stimflow.Flow(end=stimflow.PauliMap({"Z": [0, 1]}), measurement_indices=[1]), + stimflow.Flow(start=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), measurement_indices=[2]), + stimflow.Flow(end=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), measurement_indices=[2]), stimflow.Flow( - start=stimflow.PauliMap({"X": [0, 1]}), end=stimflow.PauliMap({"X": [0, 1]}), obs_key="X" + start=stimflow.PauliMap({"X": [0, 1]}).with_name("X"), end=stimflow.PauliMap({"X": [0, 1]}).with_name("X"), ), stimflow.Flow( - start=stimflow.PauliMap({"Z": [0, 1j]}), end=stimflow.PauliMap({"Z": [0, 1j]}), obs_key="Z" + start=stimflow.PauliMap({"Z": [0, 1j]}).with_name("Z"), end=stimflow.PauliMap({"Z": [0, 1j]}).with_name("Z"), ), ], ) @@ -572,14 +569,14 @@ def test_verify_distance(): """), flows=[ stimflow.Flow(start=lz, end=lz), - stimflow.Flow(start=zz01, mids=[0]), - stimflow.Flow(start=zz12, mids=[1]), - stimflow.Flow(start=zz23, mids=[2]), - stimflow.Flow(start=zz34, mids=[3]), - stimflow.Flow(end=zz01, mids=[0]), - stimflow.Flow(end=zz12, mids=[1]), - stimflow.Flow(end=zz23, mids=[2]), - stimflow.Flow(end=zz34, mids=[3]), + stimflow.Flow(start=zz01, measurement_indices=[0]), + stimflow.Flow(start=zz12, measurement_indices=[1]), + stimflow.Flow(start=zz23, measurement_indices=[2]), + stimflow.Flow(start=zz34, measurement_indices=[3]), + stimflow.Flow(end=zz01, measurement_indices=[0]), + stimflow.Flow(end=zz12, measurement_indices=[1]), + stimflow.Flow(end=zz23, measurement_indices=[2]), + stimflow.Flow(end=zz34, measurement_indices=[3]), ], ) chunk.verify_distance_is_at_least(3) diff --git a/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py b/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py index dc93a97d..4d8d725b 100644 --- a/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py @@ -29,7 +29,7 @@ def test_solve_flow_auto_measurements(): ) == [ stimflow.Flow( - start=stimflow.PauliMap({"Z": [0 + 1j, 2 + 1j]}), mids=[0], center=-1, flags={"X"} + start=stimflow.PauliMap({"Z": [0 + 1j, 2 + 1j]}), measurement_indices=[0], center=-1, flags={"X"} ), ] ) @@ -42,9 +42,8 @@ def test_solve_flow_auto_flow_measurements_with_observable(): _solve_auto_flow_ms( flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_xs([1, 2]), - end=stimflow.PauliMap.from_zs([1, 2]), - obs_key="L2", + start=stimflow.PauliMap.from_xs([1, 2]).with_name("L2"), + end=stimflow.PauliMap.from_zs([1, 2]).with_name("L2"), ) ], circuit=stim.Circuit( @@ -58,10 +57,9 @@ def test_solve_flow_auto_flow_measurements_with_observable(): ) == [ stimflow.Flow( - start=stimflow.PauliMap.from_xs([1, 2]), - end=stimflow.PauliMap.from_zs([1, 2]), - mids=[0], - obs_key="L2", + start=stimflow.PauliMap.from_xs([1, 2]).with_name("L2"), + end=stimflow.PauliMap.from_zs([1, 2]).with_name("L2"), + measurement_indices=[0], ), ] ) diff --git a/glue/stimflow/src/stimflow/_core/_flow.py b/glue/stimflow/src/stimflow/_core/_flow.py index eea10312..6cf57fbf 100644 --- a/glue/stimflow/src/stimflow/_core/_flow.py +++ b/glue/stimflow/src/stimflow/_core/_flow.py @@ -24,8 +24,7 @@ def __init__( *, start: PauliMap | Tile | None = None, end: PauliMap | Tile | None = None, - mids: Iterable[int] = (), - obs_key: Any = None, + measurement_indices: Iterable[int] = (), center: complex | None = None, flags: Iterable[Any] = frozenset(), sign: bool | None = None, @@ -37,65 +36,58 @@ def __init__( circuit (before *all* operations, including resets). end: Defaults to None (empty). The Pauli product operator at the end of the circuit (after *all* operations, including measurements). - mids: Defaults to empty. Indices of measurements that mediate the flow (that multiply + measurement_indices: Defaults to empty. Indices of measurements that mediate the flow (that multiply into it as it traverses the circuit). center: Defaults to None (unspecified). Specifies a 2d coordinate to use in metadata when the flow is completed into a detector. Incompatible with obs_key. - obs_key: Defaults to None (detector flow). Identifies that this is an observable flow - (instead of a detector flow) and gives a name that be used when linking chunks. flags: Defaults to empty. Custom information about the flow, that can be used by code operating on chunks for a variety of purposes. For example, this could identify the "color" of the flow in a color code. sign: Defaults to None (unsigned). The expected sign of the flow. """ - if obs_key is None and center is None: - if isinstance(start, Tile) and start.measure_qubit is not None: - center = start.measure_qubit - if isinstance(end, Tile) and end.measure_qubit is not None: - center = end.measure_qubit - if obs_key is None and center is None: - qubits: list[complex] = [] - if isinstance(start, PauliMap): - qubits.extend(start.keys()) - if isinstance(end, PauliMap): - qubits.extend(end.keys()) - if isinstance(start, Tile): - qubits.extend(start.data_set) - if isinstance(end, Tile): - qubits.extend(end.data_set) - center = sum(qubits) / (len(qubits) or 1) - if isinstance(flags, str): - raise TypeError(f"{flags=} is a str instead of a set") - if obs_key is None and isinstance(start, PauliMap) and start.name is not None: - obs_key = start.name - if obs_key is None and isinstance(end, PauliMap) and end.name is not None: - obs_key = end.name - if isinstance(start, PauliMap) and start.name is not None: - assert obs_key == start.name - start = start.with_name(None) - if isinstance(end, PauliMap) and end.name is not None: - assert obs_key == end.name - end = end.with_name(None) - if start is not None and not isinstance(start, (PauliMap, Tile)): - raise ValueError( + raise TypeError( f"{start=} is not None and not isinstance(start, (stimflow.PauliMap, stimflow.Tile))" ) if end is not None and not isinstance(end, (PauliMap, Tile)): - raise ValueError( + raise TypeError( f"{end=} is not None and not isinstance(end, (stimflow.PauliMap, stimflow.Tile))" ) + if isinstance(flags, str): + raise TypeError(f"{flags=} is a str instead of a set") + if isinstance(start, PauliMap) and isinstance(end, PauliMap) and start.name != end.name: + raise ValueError(f'{start.name=} != {end.name=}') + + if center is None and isinstance(start, Tile): + center = start.measure_qubit + if center is None and isinstance(end, Tile): + center = end.measure_qubit + + if isinstance(start, PauliMap): + obs_key = start.name + elif isinstance(end, PauliMap): + obs_key = end.name + else: + obs_key = None if isinstance(start, Tile): - start = start.to_pauli_map() + start = start.to_pauli_map().with_name(obs_key) elif start is None: - start = PauliMap() + start = PauliMap(name=obs_key) if isinstance(end, Tile): - end = end.to_pauli_map() + end = end.to_pauli_map().with_name(obs_key) elif end is None: - end = PauliMap() - self.start: PauliMap = start.with_name(obs_key) - self.end: PauliMap = end.with_name(obs_key) - self.measurement_indices: tuple[int, ...] = tuple(xor_sorted(mids)) + end = PauliMap(name=obs_key) + + if center is None: + qubits: list[complex] = [] + qubits.extend(start.keys()) + qubits.extend(end.keys()) + if qubits: + center = sum(qubits) / len(qubits) + + self.start: PauliMap = start + self.end: PauliMap = end + self.measurement_indices: tuple[int, ...] = tuple(xor_sorted(measurement_indices)) self.flags: frozenset[Any] = frozenset(flags) self.center: complex | None = center self.sign: bool | None = sign @@ -134,20 +126,18 @@ def with_edits( start: PauliMap = _UNSPECIFIED, end: PauliMap = _UNSPECIFIED, measurement_indices: Iterable[int] = _UNSPECIFIED, - obs_key: Any = _UNSPECIFIED, - center: complex = _UNSPECIFIED, + center: complex | None = _UNSPECIFIED, flags: Iterable[str] = _UNSPECIFIED, sign: Any = _UNSPECIFIED, ) -> Flow: return Flow( start=self.start if start is _UNSPECIFIED else start, end=self.end if end is _UNSPECIFIED else end, - mids=( + measurement_indices=( self.measurement_indices if measurement_indices is _UNSPECIFIED else cast(Any, measurement_indices) ), - obs_key=self.obs_key if obs_key is _UNSPECIFIED else obs_key, center=self.center if center is _UNSPECIFIED else center, flags=self.flags if flags is _UNSPECIFIED else flags, sign=self.sign if sign is _UNSPECIFIED else sign, @@ -224,7 +214,6 @@ def __repr__(self): f"end={self.end!r}, " f"measurement_indices={self.measurement_indices!r}, " f"flags={self.flags!r}, " - f"obs_key={self.obs_key!r}, " f"center={self.center!r}, " f"sign={self.sign!r}" ) @@ -239,7 +228,7 @@ def with_transformed_coords(self, transform: Callable[[complex], complex]) -> Fl center=None if self.center is None else transform(self.center), ) - def fuse_with_next_flow(self, next_flow: Flow, *, next_flow_measure_offset: int) -> Flow: + def fused_with_next_flow(self, next_flow: Flow, *, next_flow_measure_offset: int) -> Flow: if next_flow.start != self.end: raise ValueError("other.start != self.end") if next_flow.obs_key != self.obs_key: @@ -256,11 +245,10 @@ def fuse_with_next_flow(self, next_flow: Flow, *, next_flow_measure_offset: int) start=self.start, end=next_flow.end, center=new_center, - mids=( + measurement_indices=( *(m + next_flow_measure_offset * (m < 0) for m in self.measurement_indices), *(m + next_flow_measure_offset for m in next_flow.measurement_indices), ), - obs_key=self.obs_key, flags=self.flags | next_flow.flags, sign=( None if self.sign is None or next_flow.sign is None else self.sign ^ next_flow.sign @@ -292,7 +280,7 @@ def __mul__(self, other: Flow) -> Flow: return Flow( start=new_start, end=new_end, - mids=xor_sorted(self.measurement_indices + other.measurement_indices), + measurement_indices=xor_sorted(self.measurement_indices + other.measurement_indices), obs_key=self.obs_key, flags=self.flags | other.flags, center=new_center, From d7b5e9be1129b4df738d6006bd2a16de4efd2c5b Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 19:11:20 -0700 Subject: [PATCH 8/9] name -> obs_name --- glue/stimflow/doc/api.md | 30 ++++---- glue/stimflow/src/stimflow/_chunk/_chunk.py | 30 ++++---- .../src/stimflow/_chunk/_chunk_builder.py | 26 +++---- .../stimflow/_chunk/_chunk_builder_test.py | 8 +-- .../src/stimflow/_chunk/_chunk_compiler.py | 54 +++++++------- .../stimflow/_chunk/_chunk_compiler_test.py | 24 +++---- .../src/stimflow/_chunk/_chunk_interface.py | 10 +-- .../src/stimflow/_chunk/_chunk_reflow.py | 6 +- .../src/stimflow/_chunk/_chunk_reflow_test.py | 8 +-- .../src/stimflow/_chunk/_chunk_test.py | 12 ++-- .../src/stimflow/_chunk/_code_util.py | 17 ++--- .../src/stimflow/_chunk/_code_util_test.py | 4 +- .../src/stimflow/_chunk/_flow_util.py | 2 +- .../src/stimflow/_chunk/_flow_util_test.py | 8 +-- .../src/stimflow/_chunk/_patch_test.py | 4 +- .../src/stimflow/_chunk/_stabilizer_code.py | 14 ++-- .../stimflow/_chunk/_stabilizer_code_test.py | 72 +++++++++---------- glue/stimflow/src/stimflow/_core/_flow.py | 46 ++++++------ .../stimflow/src/stimflow/_core/_pauli_map.py | 38 +++++----- 19 files changed, 207 insertions(+), 206 deletions(-) diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 948a6992..a329d37c 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -92,7 +92,7 @@ - [`stimflow.Flow.__init__`](#stimflow.Flow.__init__) - [`stimflow.Flow.__mul__`](#stimflow.Flow.__mul__) - [`stimflow.Flow.fused_with_next_flow`](#stimflow.Flow.fused_with_next_flow) - - [`stimflow.Flow.obs_key`](#stimflow.Flow.obs_key) + - [`stimflow.Flow.obs_name`](#stimflow.Flow.obs_name) - [`stimflow.Flow.to_stim_flow`](#stimflow.Flow.to_stim_flow) - [`stimflow.Flow.with_edits`](#stimflow.Flow.with_edits) - [`stimflow.Flow.with_transformed_coords`](#stimflow.Flow.with_transformed_coords) @@ -160,7 +160,7 @@ - [`stimflow.PauliMap.to_tile`](#stimflow.PauliMap.to_tile) - [`stimflow.PauliMap.values`](#stimflow.PauliMap.values) - [`stimflow.PauliMap.with_basis`](#stimflow.PauliMap.with_basis) - - [`stimflow.PauliMap.with_name`](#stimflow.PauliMap.with_name) + - [`stimflow.PauliMap.with_obs_name`](#stimflow.PauliMap.with_obs_name) - [`stimflow.PauliMap.with_transformed_coords`](#stimflow.PauliMap.with_transformed_coords) - [`stimflow.PauliMap.with_xy_flipped`](#stimflow.PauliMap.with_xy_flipped) - [`stimflow.PauliMap.with_xz_flipped`](#stimflow.PauliMap.with_xz_flipped) @@ -566,7 +566,7 @@ def verify_distance_is_at_least( Example: >>> import stimflow as sf >>> import stim - >>> lz = sf.PauliMap({0: "Z"}).with_name("LZ") + >>> lz = sf.PauliMap({0: "Z"}).with_obs_name("LZ") >>> zz01 = sf.PauliMap.from_zs([0, 1]) >>> zz12 = sf.PauliMap.from_zs([1, 2]) >>> zz23 = sf.PauliMap.from_zs([2, 3]) @@ -697,7 +697,7 @@ class ChunkBuilder: ... stabilizer = sf.PauliMap.from_zs([m-0.5, m+0.5]) ... builder.add_flow(start=stabilizer, ms=[m]) ... builder.add_flow(end=stabilizer, ms=[m]) - >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_name("LZ") + >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_obs_name("LZ") >>> builder.add_flow(start=obs, end=obs) >>> chunk = builder.finish_chunk() @@ -1721,7 +1721,7 @@ def __init__( measurement_indices: Defaults to empty. Indices of measurements that mediate the flow (that multiply into it as it traverses the circuit). center: Defaults to None (unspecified). Specifies a 2d coordinate to use in metadata - when the flow is completed into a detector. Incompatible with obs_key. + when the flow is completed into a detector. Incompatible with obs_name. flags: Defaults to empty. Custom information about the flow, that can be used by code operating on chunks for a variety of purposes. For example, this could identify the "color" of the flow in a color code. @@ -1757,13 +1757,13 @@ def fused_with_next_flow( ) -> Flow: ``` - + ```python -# stimflow.Flow.obs_key +# stimflow.Flow.obs_name # (in class stimflow.Flow) @property -def obs_key( +def obs_name( self, ): ``` @@ -2604,13 +2604,13 @@ def __init__( self, mapping: "dict[complex, Literal['X, 'Y, 'Z'] | str] | dict[Literal['X, 'Y, 'Z'] | str, complex | Iterable[complex]] | PauliMap | Tile | stim.PauliString | None" = None, *, - name: Any = None, + obs_name: Any = None, ): """Initializes a PauliMap using maps of Paulis to/from qubits. Args: mapping: The association between qubits and paulis, specifiable in a variety of ways. - name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, + obs_name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, in order to identify the Pauli map. A common convention used in the library is that named Pauli maps correspond to logical operators. @@ -2636,8 +2636,8 @@ def __init__( >>> print(sf.PauliMap({0: "X", "Y": [0, 1]})) Z0*Y1 - >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"}, name="test")) - (name='test') X0*Y1*Z2 + >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"}, obs_name="test")) + (obs_name='test') X0*Y1*Z2 """ ``` @@ -2810,12 +2810,12 @@ def with_basis( """ ``` - + ```python -# stimflow.PauliMap.with_name +# stimflow.PauliMap.with_obs_name # (in class stimflow.PauliMap) -def with_name( +def with_obs_name( self, name: Any, ) -> PauliMap: diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk.py b/glue/stimflow/src/stimflow/_chunk/_chunk.py index 44c30bc3..e9fa1915 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk.py @@ -129,8 +129,8 @@ def __init__( ) o2i = {} for flow in flows: - if flow.obs_key is not None and flow.obs_key not in o2i: - o2i[flow.obs_key] = len(o2i) + if flow.obs_name is not None and flow.obs_name not in o2i: + o2i[flow.obs_name] = len(o2i) self.q2i: dict[complex, int] = q2i self.o2i: dict[Any, int] = o2i @@ -192,7 +192,7 @@ def _then_reflow(self, other: ChunkReflow) -> Chunk: if inp in old_discarded_outputs: new_discarded_outputs.append(out) break - f = i2f[inp].with_edits(obs_key=None) + f = i2f[inp].with_edits(obs_name=None) if acc is None: acc = f else: @@ -200,7 +200,7 @@ def _then_reflow(self, other: ChunkReflow) -> Chunk: else: assert acc is not None assert acc.end == out - new_flows.append(acc.with_edits(obs_key=out.name)) + new_flows.append(acc.with_edits(obs_name=out.obs_name)) if used_outputs != must_match_outputs: lines = ["Unmatched reflows."] for e in must_match_outputs - used_outputs: @@ -297,8 +297,8 @@ def _then_chunk(self, other: Chunk) -> Chunk: new_discarded_outputs.append(flow.end) for flow in new_flows: - if flow.obs_key is not None: - compiler.o2i.setdefault(flow.obs_key, len(compiler.o2i)) + if flow.obs_name is not None: + compiler.o2i.setdefault(flow.obs_name, len(compiler.o2i)) result = Chunk( circuit=combined_circuit, q2i=compiler.q2i, @@ -329,7 +329,7 @@ def __repr__(self) -> str: return "\n".join(lines) def with_obs_flows_as_det_flows(self) -> Chunk: - return self.with_edits(flows=[flow.with_edits(obs_key=None) for flow in self.flows]) + return self.with_edits(flows=[flow.with_edits(obs_name=None) for flow in self.flows]) def with_flag_added_to_all_flows(self, flag: str) -> Chunk: return self.with_edits( @@ -380,8 +380,8 @@ def from_circuit_with_mpp_boundaries(circuit: stim.Circuit) -> Chunk: start_pm = PauliMap(start_ps).with_transformed_coords(lambda i: i2q[i]) end_pm = PauliMap(end_ps).with_transformed_coords(lambda i: i2q[i]) if target.is_logical_observable_id(): - start_pm = start_pm.with_name(target.val) - end_pm = end_pm.with_name(target.val) + start_pm = start_pm.with_obs_name(target.val) + end_pm = end_pm.with_obs_name(target.val) center = sum(start_pm.keys()) + sum(end_pm.keys()) if center: center /= len(start_pm) + len(end_pm) @@ -445,7 +445,7 @@ def _interface( if collisions: msg = [f"{side} interface had collisions:"] for a, b in sorted(collisions): - msg.append(f" {a}, obs_key={b}") + msg.append(f" {a}, obs_name={b}") raise ValueError("\n".join(msg)) return tuple(sorted(result_set)) @@ -579,7 +579,7 @@ def verify_distance_is_at_least(self, minimum_distance: int, *, noise: float | N Example: >>> import stimflow as sf >>> import stim - >>> lz = sf.PauliMap({0: "Z"}).with_name("LZ") + >>> lz = sf.PauliMap({0: "Z"}).with_obs_name("LZ") >>> zz01 = sf.PauliMap.from_zs([0, 1]) >>> zz12 = sf.PauliMap.from_zs([1, 2]) >>> zz23 = sf.PauliMap.from_zs([2, 3]) @@ -871,13 +871,13 @@ def end_interface(self, *, skip_passthroughs: bool = False) -> ChunkInterface: def start_code(self) -> StabilizerCode: return StabilizerCode( self.start_patch(), - logicals=[flow.start for flow in self.flows if flow.obs_key is not None], + logicals=[flow.start for flow in self.flows if flow.obs_name is not None], ) def end_code(self) -> StabilizerCode: return StabilizerCode( self.end_patch(), - logicals=[flow.end for flow in self.flows if flow.obs_key is not None], + logicals=[flow.end for flow in self.flows if flow.obs_name is not None], ) def start_patch(self) -> Patch: @@ -893,7 +893,7 @@ def start_patch(self) -> Patch: ) for flow in self.flows if flow.start - if flow.obs_key is None + if flow.obs_name is None ] ) @@ -910,7 +910,7 @@ def end_patch(self) -> Patch: ) for flow in self.flows if flow.end - if flow.obs_key is None + if flow.obs_name is None ] ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py index dea3c865..20581091 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py @@ -40,7 +40,7 @@ class ChunkBuilder: ... stabilizer = sf.PauliMap.from_zs([m-0.5, m+0.5]) ... builder.add_flow(start=stabilizer, ms=[m]) ... builder.add_flow(end=stabilizer, ms=[m]) - >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_name("LZ") + >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_obs_name("LZ") >>> builder.add_flow(start=obs, end=obs) >>> chunk = builder.finish_chunk() @@ -303,18 +303,18 @@ def add_flow( f" {ms=}" f" {end=}") if isinstance(start, PauliMap): - obs_key = start.name + obs_name = start.obs_name elif isinstance(end, PauliMap): - obs_key = end.name + obs_name = end.obs_name else: - obs_key = None + obs_name = None out = self._flows if start == "auto": out = self._flows_with_auto_start - start = PauliMap(name=obs_key) + start = PauliMap(obs_name=obs_name) elif end == "auto": out = self._flows_with_auto_end - end = PauliMap(name=obs_key) + end = PauliMap(obs_name=obs_name) elif ms == "auto": out = self._flows_with_auto_ms ms = () @@ -534,32 +534,32 @@ def append( elif data.name == "DETECTOR" or data.name == "OBSERVABLE_INCLUDE": if isinstance(targets, PauliMap) and data.name == "OBSERVABLE_INCLUDE": - if arg is None and targets.name is None: + if arg is None and targets.obs_name is None: raise ValueError( "Received a stimflow.PauliMap target for an OBSERVABLE_INCLUDE instruction, but can't figure out its name.\n" "(The name is used in order to give consistent index to OBSERVABLE_INCLUDE instructions.)\n" "(The mapping is stored in the field `stimflow.ChunkBuilder.o2i`.)\n" "\n" "You can do either of the following to fix the error:\n" - " (a) Pass in a PauliMap with a name (see `stimflow.PauliMap.with_name(name)`)\n" + " (a) Pass in a PauliMap with a name (see `stimflow.PauliMap.with_obs_name(name)`)\n" " (b) Do a manual override by adding `arg=index` to the `stimflow.ChunkBuilder.append` call\n" "\n" "Note that, if you do both (a) and (b), the builder will remember the " "name-to-index association." ) - elif arg is not None and targets.name is not None: + elif arg is not None and targets.obs_name is not None: if not isinstance(arg, (int, float)) or arg != int(arg): raise ValueError(f"{arg=} isn't an integer.") - old_arg = self.o2i.get(targets.name) + old_arg = self.o2i.get(targets.obs_name) if old_arg is None: - self.o2i[targets.name] = int(arg) + self.o2i[targets.obs_name] = int(arg) elif old_arg != arg: raise ValueError( - f"Specified {arg=} and {targets=} but {self.o2i[targets.name]=} is " + f"Specified {arg=} and {targets=} but {self.o2i[targets.obs_name]=} is " f"inconsistent with {arg=}." ) elif arg is None: - arg = self._ensure_obs_index_of(targets.name) + arg = self._ensure_obs_index_of(targets.obs_name) self._ensure_indices( targets.keys(), diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py index 5891892d..a2c56f89 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py @@ -138,8 +138,8 @@ def test_make_surface_code_first_round(): tiles.append(stimflow.Tile(measure_qubit=m, data_qubits=data, bases=basis)) patch = stimflow.Patch(tiles) - obs_x = stimflow.PauliMap({q: "X" for q in patch.data_set if q.real == 0}).with_name("LX") - obs_z = stimflow.PauliMap({q: "Z" for q in patch.data_set if q.imag == 0}).with_name("LZ") + obs_x = stimflow.PauliMap({q: "X" for q in patch.data_set if q.real == 0}).with_obs_name("LX") + obs_z = stimflow.PauliMap({q: "Z" for q in patch.data_set if q.imag == 0}).with_obs_name("LZ") code = stimflow.StabilizerCode(patch, logicals=[(obs_x, obs_z)]).with_transformed_coords( lambda e: e * (1 - 1j) ) @@ -218,8 +218,8 @@ def test_skip_unknown_2qm(): def test_partial_observable_include_memory_experiment(): - obs_x = stimflow.PauliMap.from_xs([0, 1]).with_name("LX") - obs_z = stimflow.PauliMap.from_zs([0, 1j]).with_name("LZ") + obs_x = stimflow.PauliMap.from_xs([0, 1]).with_obs_name("LX") + obs_z = stimflow.PauliMap.from_zs([0, 1j]).with_obs_name("LZ") stab_z0 = stimflow.PauliMap.from_zs([0, 1]) stab_z1 = stimflow.PauliMap.from_zs([1j, 1 + 1j]) stab_x = stimflow.PauliMap.from_xs([0, 1, 1j, 1 + 1j]) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py index 0fb2d530..14a74308 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py @@ -71,13 +71,13 @@ def __str__(self) -> str: lines.append(" det_flows {") for key, flow in self.open_flows.items(): - if isinstance(flow, Flow) and flow.obs_key is None: + if isinstance(flow, Flow) and flow.obs_name is None: lines.append(f" {flow.end}, ms={flow.measurement_indices}") lines.append(" }") lines.append(" obs_flows {") for key, flow in self.open_flows.items(): - if isinstance(flow, Flow) and flow.obs_key is not None: + if isinstance(flow, Flow) and flow.obs_name is not None: lines.append(f" {flow.key_end}: ms={flow.measurement_indices}") lines.append(" }") @@ -116,13 +116,13 @@ def finish_circuit(self) -> stim.Circuit: obs2i: dict[int, int | Literal["discard"]] = {} next_obs_index = 0 - for obs_key, obs_index in sorted(self.o2i.items(), key=lambda e: e[1]): - if obs_key in self.discarded_observables: + for obs_name, obs_index in sorted(self.o2i.items(), key=lambda e: e[1]): + if obs_name in self.discarded_observables: obs2i[obs_index] = "discard" - elif isinstance(obs_key, int): + elif isinstance(obs_name, int): obs2i[obs_index] = next_obs_index next_obs_index += 1 - for obs_key, obs_index in sorted(self.o2i.items(), key=lambda e: e[1]): + for obs_name, obs_index in sorted(self.o2i.items(), key=lambda e: e[1]): if obs_index not in obs2i: obs2i[obs_index] = next_obs_index next_obs_index += 1 @@ -160,16 +160,16 @@ def append_magic_init_chunk(self, expected: ChunkInterface | None = None) -> Non self.waiting_for_magic_init = False self.ensure_qubits_included(expected.used_set) - self.ensure_observables_included(port.name for port in sorted(expected.ports)) - self.ensure_observables_included(port.name for port in sorted(expected.discards)) - obs_ports = sorted(port for port in expected.ports if port.name is not None) + self.ensure_observables_included(port.obs_name for port in sorted(expected.ports)) + self.ensure_observables_included(port.obs_name for port in sorted(expected.discards)) + obs_ports = sorted(port for port in expected.ports if port.obs_name is not None) for obs_port in obs_ports: assert obs_port not in self.open_flows - assert obs_port.name is not None + assert obs_port.obs_name is not None targets = [stim.target_pauli(self.q2i[q], p) for q, p in obs_port.items()] self.open_flows[obs_port] = Flow(end=obs_port) - obs_index = self.o2i.setdefault(obs_port.name, len(self.o2i)) - metadata = self.metadata_func(Flow(start=PauliMap(name=obs_port.name))) + obs_index = self.o2i.setdefault(obs_port.obs_name, len(self.o2i)) + metadata = self.metadata_func(Flow(start=PauliMap(obs_name=obs_port.obs_name))) self.circuit.append("OBSERVABLE_INCLUDE", targets, arg=obs_index, tag=metadata.tag) self.circuit.append("TICK") @@ -195,9 +195,9 @@ def append_magic_end_chunk(self, expected: ChunkInterface | None = None) -> None if expected is None: expected = self.cur_end_interface() self.ensure_qubits_included(expected.used_set) - self.ensure_observables_included(port.name for port in sorted(expected.ports)) - self.ensure_observables_included(port.name for port in sorted(expected.discards)) - obs_ports: list[PauliMap] = sorted(port for port in expected.ports if port.name is not None) + self.ensure_observables_included(port.obs_name for port in sorted(expected.ports)) + self.ensure_observables_included(port.obs_name for port in sorted(expected.discards)) + obs_ports: list[PauliMap] = sorted(port for port in expected.ports if port.obs_name is not None) completed_flows = [] for discarded in expected.discards: v = self.open_flows.pop(discarded) @@ -230,8 +230,8 @@ def append_magic_end_chunk(self, expected: ChunkInterface | None = None) -> None flow = flow.fused_with_next_flow( Flow(start=obs_port), next_flow_measure_offset=0 ) - obs_index = self.o2i.setdefault(obs_port.name, len(self.o2i)) - metadata = self.metadata_func(Flow(start=PauliMap(name=obs_port.name))) + obs_index = self.o2i.setdefault(obs_port.obs_name, len(self.o2i)) + metadata = self.metadata_func(Flow(start=PauliMap(obs_name=obs_port.obs_name))) self.circuit.append("OBSERVABLE_INCLUDE", targets, arg=obs_index, tag=metadata.tag) completed_flows.append(flow) self._append_detectors(completed_flows=completed_flows) @@ -268,8 +268,8 @@ def append(self, appended: Chunk | ChunkLoop | ChunkReflow) -> None: def _append_chunk_reflow(self, *, chunk_reflow: ChunkReflow) -> None: for ps in chunk_reflow.discard_in: - if ps.name is not None: - self.discarded_observables.add(ps.name) + if ps.obs_name is not None: + self.discarded_observables.add(ps.obs_name) next_flows: dict[PauliMap, Flow | Literal["discard"]] = {} for output, inputs in chunk_reflow.out2in.items(): measurements: set[int] = set() @@ -357,11 +357,11 @@ def _append_chunk(self, *, chunk: Chunk) -> None: # Attach new flows to existing flows. next_flows, completed_flows = self._compute_next_flows(chunk=chunk) for ps in chunk.discarded_inputs: - if ps.name is not None: - self.discarded_observables.add(ps.name) + if ps.obs_name is not None: + self.discarded_observables.add(ps.obs_name) for ps in chunk.discarded_outputs: - if ps.name is not None: - self.discarded_observables.add(ps.name) + if ps.obs_name is not None: + self.discarded_observables.add(ps.obs_name) self.num_measurements += chunk.circuit.num_measurements self.open_flows = next_flows @@ -487,11 +487,11 @@ def _append_detectors(self, *, completed_flows: list[Flow]): # Dump observable changes. for key, flow in list(self.open_flows.items()): - if key.name is not None and isinstance(flow, Flow) and flow.measurement_indices: + if key.obs_name is not None and isinstance(flow, Flow) and flow.measurement_indices: dump_targets: list[stim.GateTarget] = [] for m in flow.measurement_indices: dump_targets.append(stim.target_rec(m - self.num_measurements)) - obs_index = self.o2i.setdefault(flow.obs_key, len(self.o2i)) + obs_index = self.o2i.setdefault(flow.obs_name, len(self.o2i)) inserted_ops.append("OBSERVABLE_INCLUDE", dump_targets, obs_index) self.open_flows[key] = flow.with_edits(measurement_indices=[]) @@ -504,14 +504,14 @@ def _append_detectors(self, *, completed_flows: list[Flow]): stim.target_rec(self._canonicalize_measurement_index_to_negative(m)) ) metadata = self.metadata_func(flow) - if flow.obs_key is None: + if flow.obs_name is None: dt = detector_pos_usage_counts[flow.center] detector_pos_usage_counts[flow.center] += 1 coord = flow.center if flow.center is not None else -1 coords = (coord.real, coord.imag, dt, *metadata.extra_coords) inserted_ops.append("DETECTOR", rec_targets, coords, tag=metadata.tag) else: - obs_index = self.o2i.setdefault(flow.obs_key, len(self.o2i)) + obs_index = self.o2i.setdefault(flow.obs_name, len(self.o2i)) if rec_targets: if metadata.extra_coords: raise ValueError( diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py index 14b513ed..01869839 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py @@ -97,7 +97,7 @@ def test_chunk_compiler_obs_flow_eager_dump(): R 0 """ ), - flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]).with_name(0), center=0)], + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]).with_obs_name(0), center=0)], ) ) compiler.append( @@ -110,8 +110,8 @@ def test_chunk_compiler_obs_flow_eager_dump(): ), flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_zs([0]).with_name(0), - end=stimflow.PauliMap.from_zs([0]).with_name(0), + start=stimflow.PauliMap.from_zs([0]).with_obs_name(0), + end=stimflow.PauliMap.from_zs([0]).with_obs_name(0), measurement_indices=[0], center=0, ) @@ -126,7 +126,7 @@ def test_chunk_compiler_obs_flow_eager_dump(): M 0 """ ), - flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_name(0), measurement_indices=[0], center=0)], + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_obs_name(0), measurement_indices=[0], center=0)], ) ) assert compiler.finish_circuit() == stim.Circuit( @@ -248,7 +248,7 @@ def test_chunk_compiler_loop_obs(): R 0 """ ), - flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]).with_name(3), center=0)], + flows=[stimflow.Flow(end=stimflow.PauliMap.from_zs([0]).with_obs_name(3), center=0)], ) ) compiler.append( @@ -263,8 +263,8 @@ def test_chunk_compiler_loop_obs(): ), flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_zs([0]).with_name(3), - end=stimflow.PauliMap.from_zs([0]).with_name(3), + start=stimflow.PauliMap.from_zs([0]).with_obs_name(3), + end=stimflow.PauliMap.from_zs([0]).with_obs_name(3), measurement_indices=[0], center=0, ) @@ -282,7 +282,7 @@ def test_chunk_compiler_loop_obs(): M 0 """ ), - flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_name(3), measurement_indices=[0], center=0)], + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_obs_name(3), measurement_indices=[0], center=0)], ) ) assert compiler.finish_circuit() == stim.Circuit( @@ -501,8 +501,8 @@ def test_drop_observable_later(): ), q2i={0: 0, 1: 1}, flows=[ - stimflow.Flow(end=zz.with_name("a"), measurement_indices=[1]), - stimflow.Flow(end=xx.with_name("b"), measurement_indices=[0]), + stimflow.Flow(end=zz.with_obs_name("a"), measurement_indices=[1]), + stimflow.Flow(end=xx.with_obs_name("b"), measurement_indices=[0]), ], ) ) @@ -515,8 +515,8 @@ def test_drop_observable_later(): """ ), q2i={0: 0, 1: 1}, - discarded_inputs=[zz.with_name("a")], - flows=[stimflow.Flow(start=xx.with_name("b"), measurement_indices=[0])], + discarded_inputs=[zz.with_obs_name("a")], + flows=[stimflow.Flow(start=xx.with_obs_name("b"), measurement_indices=[0])], ) ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py b/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py index 0aec0f49..06462976 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py @@ -22,7 +22,7 @@ def partitioned_detector_flows(self) -> list[list[PauliMap]]: layers: collections.defaultdict[int, list[PauliMap]] = collections.defaultdict(list) for port in sorted(self.ports): - if port.name is None: + if port.obs_name is None: layer_index = 0 while any((q, layer_index) in qubit_used for q in port.keys()): layer_index += 1 @@ -88,8 +88,8 @@ def without_discards(self) -> ChunkInterface: def without_keyed(self) -> ChunkInterface: """Returns the same chunk interface, but without logical flows (named flows).""" return ChunkInterface( - ports=[port for port in self.ports if port.name is None], - discards=[discard for discard in self.discards if discard.name is None], + ports=[port for port in self.ports if port.obs_name is None], + discards=[discard for discard in self.discards if discard.obs_name is None], ) def with_discards_as_ports(self) -> ChunkInterface: @@ -151,7 +151,7 @@ def to_patch(self) -> Patch: Tile(bases="".join(port.values()), data_qubits=port.keys(), measure_qubit=None) for pauli_string_list in [self.ports, self.discards] for port in pauli_string_list - if port.name is None + if port.obs_name is None ] ) @@ -163,6 +163,6 @@ def to_code(self) -> StabilizerCode: port for pauli_string_list in [self.ports, self.discards] for port in pauli_string_list - if port.name is not None + if port.obs_name is not None ], ) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py index c6d9dd34..d27af7c5 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py @@ -102,7 +102,7 @@ def from_auto_rewrite_transitions_using_stable( unsolved: list[tuple[PauliMap, PauliMap]] = [] for inp, out in transitions: - assert inp.name == out.name + assert inp.obs_name == out.obs_name if inp == out: new_out2in[out] = [inp] else: @@ -172,7 +172,7 @@ def start_code(self) -> StabilizerCode: tiles: list[Tile] = [] observables: list[PauliMap] = [] for obs in self.removed_inputs: - if obs.name is None: + if obs.obs_name is None: tiles.append(Tile(data_qubits=obs.keys(), bases="".join(obs.values()))) else: observables.append(obs) @@ -185,7 +185,7 @@ def end_code(self) -> StabilizerCode: tiles: list[Tile] = [] observables: list[PauliMap] = [] for obs in self.out2in.keys(): - if obs.name is None: + if obs.obs_name is None: tiles.append(Tile(data_qubits=obs.keys(), bases="".join(obs.values()))) else: observables.append(obs) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py index 438c42df..8d67b562 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py @@ -44,14 +44,14 @@ def test_from_auto_rewrite_xyz(): def test_from_auto_rewrite_keyed(): result = stimflow.ChunkReflow.from_auto_rewrite( - inputs=[stimflow.PauliMap({"X": [2, 3]}), stimflow.PauliMap({"Z": [2, 3]}).with_name("test")], + inputs=[stimflow.PauliMap({"X": [2, 3]}), stimflow.PauliMap({"Z": [2, 3]}).with_obs_name("test")], out2in={stimflow.PauliMap({"Y": [2, 3]}): "auto"}, ) assert result == stimflow.ChunkReflow( out2in={ stimflow.PauliMap({"Y": [2, 3]}): [ stimflow.PauliMap({"X": [2, 3]}), - stimflow.PauliMap({"Z": [2, 3]}).with_name("test"), + stimflow.PauliMap({"Z": [2, 3]}).with_obs_name("test"), ] } ) @@ -67,5 +67,5 @@ def test_from_auto_rewrite_transitions_using_stable(): stable=[x12], transitions=[(x1, x2)] ) == stimflow.ChunkReflow(out2in={x12: [x12], x2: [x12, x1]}) assert stimflow.ChunkReflow.from_auto_rewrite_transitions_using_stable( - stable=[y12], transitions=[(z12.with_name("test"), x12.with_name("test"))] - ) == stimflow.ChunkReflow(out2in={y12: [y12], x12.with_name("test"): [y12, z12.with_name("test")]}) + stable=[y12], transitions=[(z12.with_obs_name("test"), x12.with_obs_name("test"))] + ) == stimflow.ChunkReflow(out2in={y12: [y12], x12.with_obs_name("test"): [y12, z12.with_obs_name("test")]}) diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py index 00eaf446..d9960424 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py @@ -205,8 +205,8 @@ def test_from_circuit_with_mpp_boundaries_simple(): q2i={1 + 2j: 0, 1 + 3j: 1}, flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_xs([1 + 2j]).with_name(0), - end=stimflow.PauliMap.from_zs([1 + 2j]).with_name(0), + start=stimflow.PauliMap.from_xs([1 + 2j]).with_obs_name(0), + end=stimflow.PauliMap.from_zs([1 + 2j]).with_obs_name(0), measurement_indices=(0,), center=1 + 2j, ) @@ -491,10 +491,10 @@ def test_anticommuting_obs_flows(): stimflow.Flow(start=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), measurement_indices=[2]), stimflow.Flow(end=stimflow.PauliMap({"Z": [1j, 1 + 1j]}), measurement_indices=[2]), stimflow.Flow( - start=stimflow.PauliMap({"X": [0, 1]}).with_name("X"), end=stimflow.PauliMap({"X": [0, 1]}).with_name("X"), + start=stimflow.PauliMap({"X": [0, 1]}).with_obs_name("X"), end=stimflow.PauliMap({"X": [0, 1]}).with_obs_name("X"), ), stimflow.Flow( - start=stimflow.PauliMap({"Z": [0, 1j]}).with_name("Z"), end=stimflow.PauliMap({"Z": [0, 1j]}).with_name("Z"), + start=stimflow.PauliMap({"Z": [0, 1j]}).with_obs_name("Z"), end=stimflow.PauliMap({"Z": [0, 1j]}).with_obs_name("Z"), ), ], ) @@ -546,14 +546,14 @@ def test_embedded_observables(): OBSERVABLE_INCLUDE(2) rec[-1] """ ), - flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_name("L2"))], + flows=[stimflow.Flow(start=stimflow.PauliMap.from_zs([0]).with_obs_name("L2"))], o2i={"L2": 2}, ) chunk.verify() def test_verify_distance(): - lz = stimflow.PauliMap({0: "Z"}).with_name("LZ") + lz = stimflow.PauliMap({0: "Z"}).with_obs_name("LZ") zz01 = stimflow.PauliMap.from_zs([0, 1]) zz12 = stimflow.PauliMap.from_zs([1, 2]) zz23 = stimflow.PauliMap.from_zs([2, 3]) diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util.py b/glue/stimflow/src/stimflow/_chunk/_code_util.py index be2e8f41..f558c54e 100644 --- a/glue/stimflow/src/stimflow/_chunk/_code_util.py +++ b/glue/stimflow/src/stimflow/_chunk/_code_util.py @@ -133,7 +133,8 @@ def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: # Anticommutes. return None return PauliMap( - {q: p for q, p in original.items() if q not in dissipated}, name=original.name + {q: p for q, p in original.items() if q not in dissipated}, + obs_name=original.obs_name, ) from stimflow._chunk._chunk_builder import ChunkBuilder @@ -153,12 +154,12 @@ def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: else: prev_builder.add_flow(start=tile, end=end, ms=start.keys() - end.keys()) for k, obs in enumerate(prev_code.flat_logicals): - assert obs.name is not None + assert obs.obs_name is not None end = clipped(obs, measured) if end is None: prev_builder.add_discarded_flow_input(obs) else: - prev_key2obs[obs.name] = end + prev_key2obs[obs.obs_name] = end prev_builder.add_flow(start=obs, end=end, ms=obs.keys() - end.keys()) next_builder = ChunkBuilder(next_code.data_set) @@ -173,12 +174,12 @@ def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: else: next_builder.add_flow(start=start, end=tile) for obs in next_code.flat_logicals: - assert obs.name is not None + assert obs.obs_name is not None start = clipped(obs, reset) if start is None: next_builder.add_discarded_flow_output(obs) else: - next_obs2key[obs.name] = start + next_obs2key[obs.obs_name] = start next_builder.add_flow(start=start, end=obs) prev_chunk = prev_builder.finish_chunk(wants_to_merge_with_prev=True) @@ -186,12 +187,12 @@ def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: stable=[ cast(PauliMap, flow.start) for flow in next_builder._flows - if flow.obs_key is None + if flow.obs_name is None if flow.start ], transitions=[ - (prev_key2obs[obs_key], next_obs2key[obs_key]) - for obs_key in next_obs2key.keys() & prev_key2obs.keys() + (prev_key2obs[obs_name], next_obs2key[obs_name]) + for obs_name in next_obs2key.keys() & prev_key2obs.keys() ], ) next_chunk = next_builder.finish_chunk(wants_to_merge_with_next=True) diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util_test.py b/glue/stimflow/src/stimflow/_chunk/_code_util_test.py index 88d24163..9b7d10a3 100644 --- a/glue/stimflow/src/stimflow/_chunk/_code_util_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_code_util_test.py @@ -144,8 +144,8 @@ def test_transversal_code_transition_chunk(): ], logicals=[ ( - stimflow.PauliMap.from_xs([0, 1, 2]).with_name("X"), - stimflow.PauliMap.from_zs([0j + 1, 1j + 1, 2j + 1]).with_name("Z"), + stimflow.PauliMap.from_xs([0, 1, 2]).with_obs_name("X"), + stimflow.PauliMap.from_zs([0j + 1, 1j + 1, 2j + 1]).with_obs_name("Z"), ) ], ) diff --git a/glue/stimflow/src/stimflow/_chunk/_flow_util.py b/glue/stimflow/src/stimflow/_chunk/_flow_util.py index e477ea47..ba6e9e0f 100644 --- a/glue/stimflow/src/stimflow/_chunk/_flow_util.py +++ b/glue/stimflow/src/stimflow/_chunk/_flow_util.py @@ -86,7 +86,7 @@ def _solve_auto_flow_ms( sub_o2i = collections.defaultdict(lambda: None) for k, flow in enumerate(result): flow = result[k] - has_obs_with_auto_measurements |= flow.obs_key is not None + has_obs_with_auto_measurements |= flow.obs_name is not None stim_flows.append(flow.to_stim_flow(q2i=q2i, o2i=sub_o2i)) if has_obs_with_auto_measurements and circuit.num_observables: diff --git a/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py b/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py index 4d8d725b..9d2edfab 100644 --- a/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py @@ -42,8 +42,8 @@ def test_solve_flow_auto_flow_measurements_with_observable(): _solve_auto_flow_ms( flows=[ stimflow.Flow( - start=stimflow.PauliMap.from_xs([1, 2]).with_name("L2"), - end=stimflow.PauliMap.from_zs([1, 2]).with_name("L2"), + start=stimflow.PauliMap.from_xs([1, 2]).with_obs_name("L2"), + end=stimflow.PauliMap.from_zs([1, 2]).with_obs_name("L2"), ) ], circuit=stim.Circuit( @@ -57,8 +57,8 @@ def test_solve_flow_auto_flow_measurements_with_observable(): ) == [ stimflow.Flow( - start=stimflow.PauliMap.from_xs([1, 2]).with_name("L2"), - end=stimflow.PauliMap.from_zs([1, 2]).with_name("L2"), + start=stimflow.PauliMap.from_xs([1, 2]).with_obs_name("L2"), + end=stimflow.PauliMap.from_zs([1, 2]).with_obs_name("L2"), measurement_indices=[0], ), ] diff --git a/glue/stimflow/src/stimflow/_chunk/_patch_test.py b/glue/stimflow/src/stimflow/_chunk/_patch_test.py index d1c524a0..05185d84 100644 --- a/glue/stimflow/src/stimflow/_chunk/_patch_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_patch_test.py @@ -35,8 +35,8 @@ def test_with_remaining_degrees_of_freedom_as_logicals(): stabilizers=[stimflow.PauliMap({"X": [0, 1, 2, 3]}), stimflow.PauliMap({"Z": [0, 1, 2, 3]})], logicals=[ # Not sure how stable the exact answer is. - (stimflow.PauliMap({"X": [1, 2]}, name="X1"), stimflow.PauliMap({"Z": [0, 2]}, name="Z1")), - (stimflow.PauliMap({"X": [1, 3]}, name="X2"), stimflow.PauliMap({"Z": [0, 3]}, name="Z2")), + (stimflow.PauliMap({"X": [1, 2]}, obs_name="X1"), stimflow.PauliMap({"Z": [0, 2]}, obs_name="Z1")), + (stimflow.PauliMap({"X": [1, 3]}, obs_name="X2"), stimflow.PauliMap({"Z": [0, 3]}, obs_name="Z2")), ], ) diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py index e1ed957f..97fb0c19 100644 --- a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py @@ -70,11 +70,11 @@ def __init__( seen_names = set() for obs in self.flat_logicals: - if obs.name is None: + if obs.obs_name is None: raise ValueError(f"Unnamed logical operator: {obs!r}") - if obs.name in seen_names: - raise ValueError(f"Name collision {obs.name=}") - seen_names.add(obs.name) + if obs.obs_name in seen_names: + raise ValueError(f"Name collision {obs.obs_name=}") + seen_names.add(obs.obs_name) @property def patch(self) -> Patch: @@ -135,7 +135,7 @@ def with_remaining_degrees_of_freedom_as_logicals(self) -> StabilizerCode: x = full_tableau.x_output(k) x2 = PauliMap(x).with_transformed_coords(cast(Any, i2q.__getitem__)) z2 = PauliMap(z).with_transformed_coords(cast(Any, i2q.__getitem__)) - new_logicals.append((x2.with_name(f"inferred_X{k}"), z2.with_name(f"inferred_Z{k}"))) + new_logicals.append((x2.with_obs_name(f"inferred_X{k}"), z2.with_obs_name(f"inferred_Z{k}"))) return StabilizerCode( stabilizers=self.patch, @@ -202,7 +202,7 @@ def concatenated_obs(over_obs: PauliMap, under_index: int) -> PauliMap: total *= obs.with_transformed_coords( lambda e: q.real * pitch.real + q.imag * pitch.imag * 1j + e ) - return total.with_name((over_obs.name, under_index)) + return total.with_obs_name((over_obs.obs_name, under_index)) new_stabilizers = [] for stabilizer in over.stabilizers: @@ -248,7 +248,7 @@ def get_observable_by_basis( if len(b1) == 1 and len(b2) == 1: # For example, we have X and Z specified and the user asked for Y. # Note that this works even if the X doesn't exactly overlap the Z. - return (a1 * a2).with_name((a1.name, a2.name)) + return (a1 * a2).with_obs_name((a1.obs_name, a2.obs_name)) if default != "__!not_specified": return default raise ValueError(f"Couldn't return a basis {basis} observable from {obs=}.") diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py index 17ec4e40..cf0828e0 100644 --- a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code_test.py @@ -12,13 +12,13 @@ def test_make_phenom_circuit_for_stabilizer_code(): stimflow.Tile(bases="X", data_qubits=[0 + 1j, 1 + 1j], measure_qubit=0.5 + 1j), ] ) - obs_x = stimflow.PauliMap({0: "X", 1j: "X"}).with_name("LX") - obs_z = stimflow.PauliMap({0: "Z", 1: "Z"}).with_name("LZ") + obs_x = stimflow.PauliMap({0: "X", 1j: "X"}).with_obs_name("LX") + obs_z = stimflow.PauliMap({0: "Z", 1: "Z"}).with_obs_name("LZ") assert stimflow.StabilizerCode(stabilizers=patch, logicals=[(obs_x, obs_z)]).make_phenom_circuit( noise=stimflow.NoiseRule(flip_result=0.125, after={"DEPOLARIZE1": 0.25}), rounds=100, - metadata_func=lambda flow: stimflow.FlowMetadata(tag=(flow.obs_key or "") + "A"), + metadata_func=lambda flow: stimflow.FlowMetadata(tag=(flow.obs_name or "") + "A"), ) == stim.Circuit( """ QUBIT_COORDS(0, 0) 0 @@ -68,14 +68,14 @@ def test_make_code_capacity_circuit_for_stabilizer_code(): stimflow.Tile(bases="X", data_qubits=[0 + 1j, 1 + 1j], measure_qubit=0.5 + 1j), ] ) - obs_x = stimflow.PauliMap({0: "X", 1j: "X"}).with_name("LX") - obs_z = stimflow.PauliMap({0: "Z", 1: "Z"}).with_name("LZ") + obs_x = stimflow.PauliMap({0: "X", 1j: "X"}).with_obs_name("LX") + obs_z = stimflow.PauliMap({0: "Z", 1: "Z"}).with_obs_name("LZ") assert stimflow.StabilizerCode( stabilizers=patch, logicals=[(obs_x, obs_z)] ).make_code_capacity_circuit( noise=stimflow.NoiseRule(after={"DEPOLARIZE1": 0.25}), - metadata_func=lambda flow: stimflow.FlowMetadata(tag=(flow.obs_key or "") + "B"), + metadata_func=lambda flow: stimflow.FlowMetadata(tag=(flow.obs_name or "") + "B"), ) == stim.Circuit( """ QUBIT_COORDS(0, 0) 0 @@ -127,8 +127,8 @@ def test_verify_distance_is_at_least_3(): stabilizers=stimflow.Patch([stimflow.Tile(bases="XXXX", data_qubits=[0, 1, 2, 3])]), logicals=[ ( - stimflow.PauliMap.from_xs([0, 1]).with_name("LX"), - stimflow.PauliMap.from_zs([0, 2]).with_name("LZ"), + stimflow.PauliMap.from_xs([0, 1]).with_obs_name("LX"), + stimflow.PauliMap.from_zs([0, 2]).with_obs_name("LZ"), ) ], ) @@ -146,8 +146,8 @@ def test_verify_distance_is_at_least_3(): ), logicals=[ ( - stimflow.PauliMap.from_xs([0, 1]).with_name("LX"), - stimflow.PauliMap.from_zs([0, 2]).with_name("LZ"), + stimflow.PauliMap.from_xs([0, 1]).with_obs_name("LX"), + stimflow.PauliMap.from_zs([0, 2]).with_obs_name("LZ"), ) ], ) @@ -166,8 +166,8 @@ def test_verify_distance_is_at_least_3(): ), logicals=[ ( - stimflow.PauliMap.from_xs([0, 1, 2, 3, 4]).with_name("LX"), - stimflow.PauliMap.from_zs([0, 1, 2, 3, 4]).with_name("LZ"), + stimflow.PauliMap.from_xs([0, 1, 2, 3, 4]).with_obs_name("LX"), + stimflow.PauliMap.from_zs([0, 1, 2, 3, 4]).with_obs_name("LZ"), ) ], ) @@ -183,12 +183,12 @@ def test_with_integer_coordinates(): ], logicals=[ ( - stimflow.PauliMap.from_xs([0, 1]).with_name("LX1"), - stimflow.PauliMap.from_zs([0, 1j]).with_name("LZ1"), + stimflow.PauliMap.from_xs([0, 1]).with_obs_name("LX1"), + stimflow.PauliMap.from_zs([0, 1j]).with_obs_name("LZ1"), ), ( - stimflow.PauliMap.from_xs([0, 1j]).with_name("LX2"), - stimflow.PauliMap.from_zs([0, 1]).with_name("LZ2"), + stimflow.PauliMap.from_xs([0, 1j]).with_obs_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_obs_name("LZ2"), ), ], ) @@ -201,12 +201,12 @@ def test_with_integer_coordinates(): ], logicals=[ ( - stimflow.PauliMap.from_xs([0, 1]).with_name("LX1"), - stimflow.PauliMap.from_zs([0, 2j]).with_name("LZ1"), + stimflow.PauliMap.from_xs([0, 1]).with_obs_name("LX1"), + stimflow.PauliMap.from_zs([0, 2j]).with_obs_name("LZ1"), ), ( - stimflow.PauliMap.from_xs([0, 2j]).with_name("LX2"), - stimflow.PauliMap.from_zs([0, 1]).with_name("LZ2"), + stimflow.PauliMap.from_xs([0, 2j]).with_obs_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_obs_name("LZ2"), ), ], ) @@ -220,12 +220,12 @@ def test_physical_to_logical(): ], logicals=[ ( - stimflow.PauliMap.from_xs([0, 1]).with_name("LX1"), - stimflow.PauliMap.from_zs([0, 1j]).with_name("LZ1"), + stimflow.PauliMap.from_xs([0, 1]).with_obs_name("LX1"), + stimflow.PauliMap.from_zs([0, 1j]).with_obs_name("LZ1"), ), ( - stimflow.PauliMap.from_xs([0, 1j]).with_name("LX2"), - stimflow.PauliMap.from_zs([0, 1]).with_name("LZ2"), + stimflow.PauliMap.from_xs([0, 1j]).with_obs_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_obs_name("LZ2"), ), ], ) @@ -252,12 +252,12 @@ def test_concat_over(): stabilizers=[stimflow.PauliMap.from_xs([a, b, c, d]), stimflow.PauliMap.from_zs([a, b, c, d])], logicals=[ ( - stimflow.PauliMap.from_xs([a, b]).with_name("LX1"), - stimflow.PauliMap.from_zs([a, c]).with_name("LX2"), + stimflow.PauliMap.from_xs([a, b]).with_obs_name("LX1"), + stimflow.PauliMap.from_zs([a, c]).with_obs_name("LX2"), ), ( - stimflow.PauliMap.from_zs([a, b]).with_name("LZ1"), - stimflow.PauliMap.from_xs([a, c]).with_name("LZ2"), + stimflow.PauliMap.from_zs([a, b]).with_obs_name("LZ1"), + stimflow.PauliMap.from_xs([a, c]).with_obs_name("LZ2"), ), ], ) @@ -278,12 +278,12 @@ def test_to_svg(): stabilizers=[stimflow.PauliMap.from_xs([a, b, c, d]), stimflow.PauliMap.from_zs([a, b, c, d])], logicals=[ ( - stimflow.PauliMap.from_xs([a, b]).with_name("LX1"), - stimflow.PauliMap.from_zs([a, c]).with_name("LZ1"), + stimflow.PauliMap.from_xs([a, b]).with_obs_name("LX1"), + stimflow.PauliMap.from_zs([a, c]).with_obs_name("LZ1"), ), ( - stimflow.PauliMap.from_zs([a, b]).with_name("LX2"), - stimflow.PauliMap.from_xs([a, c]).with_name("LZ2"), + stimflow.PauliMap.from_zs([a, b]).with_obs_name("LX2"), + stimflow.PauliMap.from_xs([a, c]).with_obs_name("LZ2"), ), ], ) @@ -303,12 +303,12 @@ def test_with_remaining_degrees_of_freedom_as_logicals(): logicals=[ # Not sure how stable the exact answer is. ( - stimflow.PauliMap({"X": [1, 2]}).with_name("inferred_X0"), - stimflow.PauliMap({"Z": [0, 2]}).with_name("inferred_Z0"), + stimflow.PauliMap({"X": [1, 2]}).with_obs_name("inferred_X0"), + stimflow.PauliMap({"Z": [0, 2]}).with_obs_name("inferred_Z0"), ), ( - stimflow.PauliMap({"X": [1, 3]}).with_name("inferred_X1"), - stimflow.PauliMap({"Z": [0, 3]}).with_name("inferred_Z1"), + stimflow.PauliMap({"X": [1, 3]}).with_obs_name("inferred_X1"), + stimflow.PauliMap({"Z": [0, 3]}).with_obs_name("inferred_Z1"), ), ], ) diff --git a/glue/stimflow/src/stimflow/_core/_flow.py b/glue/stimflow/src/stimflow/_core/_flow.py index 6cf57fbf..4831bec2 100644 --- a/glue/stimflow/src/stimflow/_core/_flow.py +++ b/glue/stimflow/src/stimflow/_core/_flow.py @@ -39,7 +39,7 @@ def __init__( measurement_indices: Defaults to empty. Indices of measurements that mediate the flow (that multiply into it as it traverses the circuit). center: Defaults to None (unspecified). Specifies a 2d coordinate to use in metadata - when the flow is completed into a detector. Incompatible with obs_key. + when the flow is completed into a detector. Incompatible with obs_name. flags: Defaults to empty. Custom information about the flow, that can be used by code operating on chunks for a variety of purposes. For example, this could identify the "color" of the flow in a color code. @@ -55,8 +55,8 @@ def __init__( ) if isinstance(flags, str): raise TypeError(f"{flags=} is a str instead of a set") - if isinstance(start, PauliMap) and isinstance(end, PauliMap) and start.name != end.name: - raise ValueError(f'{start.name=} != {end.name=}') + if isinstance(start, PauliMap) and isinstance(end, PauliMap) and start.obs_name != end.obs_name: + raise ValueError(f'{start.obs_name=} != {end.obs_name=}') if center is None and isinstance(start, Tile): center = start.measure_qubit @@ -64,19 +64,19 @@ def __init__( center = end.measure_qubit if isinstance(start, PauliMap): - obs_key = start.name + obs_name = start.obs_name elif isinstance(end, PauliMap): - obs_key = end.name + obs_name = end.obs_name else: - obs_key = None + obs_name = None if isinstance(start, Tile): - start = start.to_pauli_map().with_name(obs_key) + start = start.to_pauli_map().with_obs_name(obs_name) elif start is None: - start = PauliMap(name=obs_key) + start = PauliMap(obs_name=obs_name) if isinstance(end, Tile): - end = end.to_pauli_map().with_name(obs_key) + end = end.to_pauli_map().with_obs_name(obs_name) elif end is None: - end = PauliMap(name=obs_key) + end = PauliMap(obs_name=obs_name) if center is None: qubits: list[complex] = [] @@ -99,12 +99,12 @@ def to_stim_flow( if self.sign: out.sign = -1 included_observables: list[int] | None - if self.obs_key is None: + if self.obs_name is None: included_observables = None elif o2i is None: - raise ValueError(f"{self.obs_key=} is not None but {o2i=}") + raise ValueError(f"{self.obs_name=} is not None but {o2i=}") else: - v = o2i[self.obs_key] + v = o2i[self.obs_name] if v is None: included_observables = None else: @@ -117,8 +117,8 @@ def to_stim_flow( ) @property - def obs_key(self) -> Any: - return self.start.name + def obs_name(self) -> Any: + return self.start.obs_name def with_edits( self, @@ -150,7 +150,7 @@ def __eq__(self, other: Any) -> bool: self.start == other.start and self.end == other.end and self.measurement_indices == other.measurement_indices - and self.obs_key == other.obs_key + and self.obs_name == other.obs_name and self.flags == other.flags and self.center == other.center and self.sign == other.sign @@ -162,7 +162,7 @@ def __hash__(self) -> int: self.start, self.end, self.measurement_indices, - self.obs_key, + self.obs_name, self.flags, self.center, self.sign, @@ -196,7 +196,7 @@ def __str__(self) -> str: if not end_terms: end_terms.append("1") - key = "" if self.obs_key is None else f" (obs={self.obs_key})" + key = "" if self.obs_name is None else f" (obs={self.obs_name})" result = f'{"*".join(start_terms)} -> {"*".join(end_terms)}{key}' if self.sign is None: pass @@ -231,8 +231,8 @@ def with_transformed_coords(self, transform: Callable[[complex], complex]) -> Fl def fused_with_next_flow(self, next_flow: Flow, *, next_flow_measure_offset: int) -> Flow: if next_flow.start != self.end: raise ValueError("other.start != self.end") - if next_flow.obs_key != self.obs_key: - raise ValueError("other.obs_key != self.obs_key") + if next_flow.obs_name != self.obs_name: + raise ValueError("other.obs_name != self.obs_name") if self.center is None: new_center = next_flow.center elif next_flow.center is None: @@ -260,8 +260,8 @@ def __mul__(self, other: Flow) -> Flow: The product of A -> B and C -> D is (A*C) -> (B*D). """ - if self.obs_key != other.obs_key: - raise ValueError(f"{self.obs_key=} != {other.obs_key=}") + if self.obs_name != other.obs_name: + raise ValueError(f"{self.obs_name=} != {other.obs_name=}") if (self.sign is None) != (other.sign is None): raise ValueError(f"({self.sign=} is None) != ({other.sign=} is None)") @@ -281,7 +281,7 @@ def __mul__(self, other: Flow) -> Flow: start=new_start, end=new_end, measurement_indices=xor_sorted(self.measurement_indices + other.measurement_indices), - obs_key=self.obs_key, + obs_name=self.obs_name, flags=self.flags | other.flags, center=new_center, sign=(None if self.sign is None else self.sign ^ other.sign), diff --git a/glue/stimflow/src/stimflow/_core/_pauli_map.py b/glue/stimflow/src/stimflow/_core/_pauli_map.py index 8ef80119..3bb23e86 100644 --- a/glue/stimflow/src/stimflow/_core/_pauli_map.py +++ b/glue/stimflow/src/stimflow/_core/_pauli_map.py @@ -51,13 +51,13 @@ def __init__( | None ) = None, *, - name: Any = None, + obs_name: Any = None, ): """Initializes a PauliMap using maps of Paulis to/from qubits. Args: mapping: The association between qubits and paulis, specifiable in a variety of ways. - name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, + obs_name: Defaults to None (no name). Can be set to an arbitrary hashable equatable value, in order to identify the Pauli map. A common convention used in the library is that named Pauli maps correspond to logical operators. @@ -83,12 +83,12 @@ def __init__( >>> print(sf.PauliMap({0: "X", "Y": [0, 1]})) Z0*Y1 - >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"}, name="test")) - (name='test') X0*Y1*Z2 + >>> print(sf.PauliMap({0: "X", 1: "Y", 2: "Z"}, obs_name="test")) + (obs_name='test') X0*Y1*Z2 """ self._dict: dict[complex, Literal["X", "Y", "Z"]] - self.name: Any = name + self.obs_name: Any = obs_name self._hash: int from stimflow._core._tile import Tile @@ -120,22 +120,22 @@ def __init__( self._dict = {complex(q): self._dict[q] for q in sorted_complex(self.keys())} else: self._dict = {} - self._hash = hash((self.name, tuple(self._dict.items()))) + self._hash = hash((self.obs_name, tuple(self._dict.items()))) @staticmethod def from_xs(xs: Iterable[complex], *, name: Any = None) -> PauliMap: """Returns a PauliMap mapping the given qubits to the X basis.""" - return PauliMap({"X": xs}, name=name) + return PauliMap({"X": xs}, obs_name=name) @staticmethod def from_ys(ys: Iterable[complex], *, name: Any = None) -> PauliMap: """Returns a PauliMap mapping the given qubits to the Y basis.""" - return PauliMap({"Y": ys}, name=name) + return PauliMap({"Y": ys}, obs_name=name) @staticmethod def from_zs(zs: Iterable[complex], *, name: Any = None) -> PauliMap: """Returns a PauliMap mapping the given qubits to the Z basis.""" - return PauliMap({"Z": zs}, name=name) + return PauliMap({"Z": zs}, obs_name=name) def __contains__(self, item: complex) -> bool: """Determines if the PauliMap maps the given qubit to a non-identity Pauli.""" @@ -165,12 +165,12 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[complex]: return self._dict.__iter__() - def with_name(self, name: Any) -> PauliMap: + def with_obs_name(self, name: Any) -> PauliMap: """Returns the same PauliMap, but with the given name. Names are used to identify logical operators. """ - return PauliMap(self, name=name) + return PauliMap(self, obs_name=name) def _mul_term(self, q: complex, b: Literal["X", "Y", "Z"]): new_b = _multiplication_table[self._dict.pop(q, None)][b] @@ -179,7 +179,7 @@ def _mul_term(self, q: complex, b: Literal["X", "Y", "Z"]): def with_basis(self, basis: Literal["X", "Y", "Z"]) -> PauliMap: """Returns the same PauliMap, but with all its qubits mapped to the given basis.""" - return PauliMap({q: basis for q in self.keys()}, name=self.name) + return PauliMap({q: basis for q in self.keys()}, obs_name=self.obs_name) def __bool__(self) -> bool: return bool(self._dict) @@ -206,10 +206,10 @@ def __mul__(self, other: PauliMap | Tile) -> PauliMap: return PauliMap(result) def __repr__(self) -> str: - if self.name is None: + if self.obs_name is None: s2 = "" else: - s2 = f", name={self.name!r}" + s2 = f", obs_name={self.obs_name!r}" qs = sorted_complex(self._dict) if len(self) > 1: p = set(self.values()) @@ -231,19 +231,19 @@ def simplify(c: complex) -> str: result = "*".join(f"{self._dict[q]}{simplify(q)}" for q in sorted_complex(self.keys())) if not result: result = 'I' - if self.name is not None: - result = f"(name={self.name!r}) " + result + if self.obs_name is not None: + result = f"(obs_name={self.obs_name!r}) " + result return result def with_xz_flipped(self) -> PauliMap: """Returns the same PauliMap, but with all qubits conjugated by H.""" remap = {"X": "Z", "Y": "Y", "Z": "X"} - return PauliMap({q: remap[p] for q, p in self._dict.items()}, name=self.name) + return PauliMap({q: remap[p] for q, p in self._dict.items()}, obs_name=self.obs_name) def with_xy_flipped(self) -> PauliMap: """Returns the same PauliMap, but with all qubits conjugated by H_XY.""" remap = {"X": "Y", "Y": "X", "Z": "Z"} - return PauliMap({q: remap[p] for q, p in self._dict.items()}, name=self.name) + return PauliMap({q: remap[p] for q, p in self._dict.items()}, obs_name=self.obs_name) def commutes(self, other: PauliMap) -> bool: """Determines if the pauli map commutes with another pauli map.""" @@ -258,7 +258,7 @@ def anticommutes(self, other: PauliMap) -> bool: def with_transformed_coords(self, transform: Callable[[complex], complex]) -> PauliMap: """Returns the same PauliMap but with coordinates transformed by the given function.""" - return PauliMap({transform(q): p for q, p in self._dict.items()}, name=self.name) + return PauliMap({transform(q): p for q, p in self._dict.items()}, obs_name=self.obs_name) def to_stim_pauli_string( self, q2i: dict[complex, int], *, num_qubits: int | None = None From cbe82bb8958616aeed0c744c65f0a507acc75741 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 12 May 2026 19:43:42 -0700 Subject: [PATCH 9/9] lookup_mids -> lookup_measurement_indices --- .gitignore | 4 +- glue/stimflow/README.md | 6 +- glue/stimflow/doc/api.md | 214 +++++++++++++---- glue/stimflow/doc/getting_started.ipynb | 6 +- .../src/stimflow/_chunk/_chunk_builder.py | 219 +++++++++++++++--- .../stimflow/_chunk/_chunk_builder_test.py | 50 ++-- .../src/stimflow/_chunk/_chunk_compiler.py | 4 +- .../src/stimflow/_chunk/_code_util.py | 4 +- .../src/stimflow/_chunk/_flow_util.py | 4 +- .../src/stimflow/_chunk/_stabilizer_code.py | 4 +- 10 files changed, 395 insertions(+), 120 deletions(-) diff --git a/.gitignore b/.gitignore index 76441ea0..b1f383f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -.idea/* -cmake-build-debug/* +.idea +cmake-build-debug a.out perf.data perf.data.old diff --git a/glue/stimflow/README.md b/glue/stimflow/README.md index 55490c44..0aa0e29b 100644 --- a/glue/stimflow/README.md +++ b/glue/stimflow/README.md @@ -97,8 +97,8 @@ def make_idle_round(d: int) -> sf.Chunk: # Assert the circuit should be preparing and measuring the stabilizers. for tile in code.tiles: - builder.add_flow(start=tile, ms=[tile.measure_qubit]) - builder.add_flow(end=tile, ms=[tile.measure_qubit]) + builder.add_flow(start=tile, measurements=[tile.measure_qubit]) + builder.add_flow(end=tile, measurements=[tile.measure_qubit]) # Assert the circuit should be preserving the logical operators. for obs in code.flat_logicals: builder.add_flow(start=obs, end=obs) @@ -136,4 +136,4 @@ def main(): if __name__ == "__main__": main() -``` \ No newline at end of file +``` diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index a329d37c..c1fdca60 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -35,8 +35,7 @@ - [`stimflow.ChunkBuilder.append_feedback`](#stimflow.ChunkBuilder.append_feedback) - [`stimflow.ChunkBuilder.finish_chunk`](#stimflow.ChunkBuilder.finish_chunk) - [`stimflow.ChunkBuilder.has_measurement`](#stimflow.ChunkBuilder.has_measurement) - - [`stimflow.ChunkBuilder.lookup_mids`](#stimflow.ChunkBuilder.lookup_mids) - - [`stimflow.ChunkBuilder.record_measurement_group`](#stimflow.ChunkBuilder.record_measurement_group) + - [`stimflow.ChunkBuilder.lookup_measurement_indices`](#stimflow.ChunkBuilder.lookup_measurement_indices) - [`stimflow.ChunkCompiler`](#stimflow.ChunkCompiler) - [`stimflow.ChunkCompiler.__init__`](#stimflow.ChunkCompiler.__init__) - [`stimflow.ChunkCompiler.append`](#stimflow.ChunkCompiler.append) @@ -695,8 +694,8 @@ class ChunkBuilder: >>> builder.append("M", measure_qubits) >>> for m in measure_qubits: ... stabilizer = sf.PauliMap.from_zs([m-0.5, m+0.5]) - ... builder.add_flow(start=stabilizer, ms=[m]) - ... builder.add_flow(end=stabilizer, ms=[m]) + ... builder.add_flow(start=stabilizer, measurements=[m]) + ... builder.add_flow(end=stabilizer, measurements=[m]) >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_obs_name("LZ") >>> builder.add_flow(start=obs, end=obs) >>> chunk = builder.finish_chunk() @@ -762,10 +761,11 @@ def __init__( allowed_qubits: Defaults to None (everything allowed). Specifies the qubit positions that the circuit is permitted to contain. - >>> import stimflow as sf - >>> data_qubits = range(5) - >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] - >>> builder = sf.ChunkBuilder(allowed_qubits=[*data_qubits, *measure_qubits]) + Examples: + >>> import stimflow as sf + >>> data_qubits = range(5) + >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] + >>> builder = sf.ChunkBuilder(allowed_qubits=[*data_qubits, *measure_qubits]) """ ``` @@ -778,6 +778,65 @@ def add_discarded_flow_input( self, flow: PauliMap | Tile, ) -> None: + """Annotates that an input stabilizer won't be used. + + When compiling chunks, it is normally an error if the output flows of one + chunk don't match up with the input flows of the next. For example, a + Z basis transversal measurement can't measure the X stabilizers of a code, + so a chunk performing can't declare flows with X basis inputs. But this + would cause an error during compilation, due the prior idling chunk having + X basis output flows. Adding the X basis stabilizers as discarded flow inputs + of the transversal chunk explicitly indicates that it is expected for this + mismatch to occur, so that no error is raised. + + Example: + >>> import stimflow as sf + >>> xx = sf.PauliMap.from_xs([0, 1]) + >>> zz = sf.PauliMap.from_zs([0, 1]) + + >>> init_builder = sf.ChunkBuilder() + >>> init_builder.append("R", [0, 1]) + >>> init_builder.add_flow(end=zz) + >>> init_builder.add_discarded_flow_output(sf.PauliMap.from_xs([0, 1])) + >>> init_chunk = init_builder.finish_chunk() + >>> init_chunk.verify() + + >>> idle_builder = sf.ChunkBuilder() + >>> idle_builder.append("MXX", [(0, 1)], measure_key_func=lambda e: ('X', e)) + >>> idle_builder.append("TICK") + >>> idle_builder.append("MZZ", [(0, 1)], measure_key_func=lambda e: ('Z', e)) + >>> idle_builder.add_flow(start=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(start=zz, measurements=[('Z', (0, 1))]) + >>> idle_builder.add_flow(end=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(end=zz, measurements=[('Z', (0, 1))]) + >>> idle_chunk = idle_builder.finish_chunk() + >>> idle_chunk.verify() + + >>> end_builder = sf.ChunkBuilder() + >>> end_builder.append("M", [0, 1]) + >>> end_builder.add_flow(start=zz, measurements=[0, 1]) + >>> end_builder.add_discarded_flow_input(sf.PauliMap.from_xs([0, 1])) + >>> end_chunk = end_builder.finish_chunk() + >>> end_chunk.verify() + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(init_chunk) + >>> compiler.append(idle_chunk) + >>> compiler.append(end_chunk) + >>> print(compiler.finish_circuit()) + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + R 0 1 + TICK + MXX 0 1 + TICK + MZZ 0 1 + DETECTOR(0.5, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + M 0 1 + DETECTOR(0.5, 0, 0) rec[-3] rec[-2] rec[-1] + """ ``` @@ -789,6 +848,65 @@ def add_discarded_flow_output( self, flow: PauliMap | Tile, ) -> None: + """Annotates that an output stabilizer won't be used. + + When compiling chunks, it is normally an error if the output flows of one + chunk don't match up with the input flows of the next. For example, a + Z basis transversal preparation can't prepare the X stabilizers of a code, + so a chunk performing can't declare flows with X basis inputs. But this + would cause an error during compilation, due the next idling chunk having + X basis input flows. Adding the X basis stabilizers as discarded flow outputs + of the transversal chunk explicitly indicates that it is expected for this + mismatch to occur, so that no error is raised. + + Example: + >>> import stimflow as sf + >>> xx = sf.PauliMap.from_xs([0, 1]) + >>> zz = sf.PauliMap.from_zs([0, 1]) + + >>> init_builder = sf.ChunkBuilder() + >>> init_builder.append("R", [0, 1]) + >>> init_builder.add_flow(end=zz) + >>> init_builder.add_discarded_flow_output(sf.PauliMap.from_xs([0, 1])) + >>> init_chunk = init_builder.finish_chunk() + >>> init_chunk.verify() + + >>> idle_builder = sf.ChunkBuilder() + >>> idle_builder.append("MXX", [(0, 1)], measure_key_func=lambda e: ('X', e)) + >>> idle_builder.append("TICK") + >>> idle_builder.append("MZZ", [(0, 1)], measure_key_func=lambda e: ('Z', e)) + >>> idle_builder.add_flow(start=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(start=zz, measurements=[('Z', (0, 1))]) + >>> idle_builder.add_flow(end=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(end=zz, measurements=[('Z', (0, 1))]) + >>> idle_chunk = idle_builder.finish_chunk() + >>> idle_chunk.verify() + + >>> end_builder = sf.ChunkBuilder() + >>> end_builder.append("M", [0, 1]) + >>> end_builder.add_flow(start=zz, measurements=[0, 1]) + >>> end_builder.add_discarded_flow_input(sf.PauliMap.from_xs([0, 1])) + >>> end_chunk = end_builder.finish_chunk() + >>> end_chunk.verify() + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(init_chunk) + >>> compiler.append(idle_chunk) + >>> compiler.append(end_chunk) + >>> print(compiler.finish_circuit()) + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + R 0 1 + TICK + MXX 0 1 + TICK + MZZ 0 1 + DETECTOR(0.5, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + M 0 1 + DETECTOR(0.5, 0, 0) rec[-3] rec[-2] rec[-1] + """ ``` @@ -801,8 +919,8 @@ def add_flow( *, start: "PauliMap | Tile | Literal['auto'] | None" = None, end: "PauliMap | Tile | Literal['auto'] | None" = None, - ms: "Iterable[Any] | Literal['auto']" = (), - ignore_unmatched_ms: bool = False, + measurements: "Iterable[Any] | Literal['auto']" = (), + ignore_unknown_measurements: bool = False, center: complex | None = None, flags: Iterable[str] = frozenset(), sign: bool | None = None, @@ -818,15 +936,15 @@ def add_flow( end: Defaults to None (empty). The stabilizer that the flow ends as, at the end of the circuit. If the flow ends within the circuit, this should be set to None or an empty PauliMap. - ms: Defaults to empty. The keys identifying measurements mediate the flow. + measurements: Defaults to empty. The keys identifying measurements mediate the flow. For example, if a stabilizer is measured by a circuit then this would typically be a singleton list containing the measurement that reveals the stabilizer's value. - ignore_unmatched_ms: Defaults to False. When set to False, unrecognized measurement + ignore_unknown_measurements: Defaults to False. When set to False, unrecognized measurement ids cause the method to raise an exception instead of adding the flow. When set to True, unrecognized measurements are silently discarded. center: Defaults to None (unused). Optional metadata specifying coordinates for the - flow. Typically these coordinates will end up being exposed as the parens args + flow. Typically, these coordinates will end up being exposed as the parens args on the DETECTOR instruction created when producing a stim circuit. When not specified, the coordinates will instead be inferred in some heuristic way. flags: Defaults to empty. Hashable equatable values associated with the flow. When @@ -845,9 +963,9 @@ def add_flow( >>> builder.append('TICK') >>> builder.append('CX', [(1j, 0)]) - >>> builder.add_flow(end=sf.PauliMap.from_xs([0, 1j]), ms=[1j]) + >>> builder.add_flow(end=sf.PauliMap.from_xs([0, 1j]), measurements=[1j]) >>> builder.add_flow(end=sf.PauliMap.from_zs([0, 1j])) - >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), ms=[1j]) + >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), measurements=[1j]) >>> builder.finish_chunk().verify() """ @@ -905,7 +1023,7 @@ def append( stored in the measurement tracker keyed by the position of the qubit being measured (or by a custom key, if `measure_key_func` is specified). The indices of the measurements can be looked up later via - `builder.lookup_mids([key1, key2, ...])`. + `builder.lookup_measurement_indices([key1, key2, ...])`. Args: gate: The name of the gate to append, such as "H" or "M" or "CX". @@ -923,7 +1041,7 @@ def append( qubit multiple times. This function can transform that position into a different value (for example, you might set `measure_key_func=lambda pos: (pos, 'first_cycle')` for - measurements during the first cycle of the circuit. + measurements during the first cycle of the circuit). tag: Defaults to "" (no tag). A custom tag to attach to the instruction(s) appended into the stim circuit. unknown_qubit_append_mode: Defaults to 'auto'. The available options are: @@ -981,51 +1099,65 @@ def has_measurement( self, key: Any, ) -> bool: + """Determines if a measurement with the given key has been performed. + + Args: + key: The measurement key. + + Returns: + Whether a measurement with the given key has been performed. + + Examples: + >>> import stimflow as sf + >>> builder = sf.ChunkBuilder() + >>> builder.append("M", [1 + 2j]) + >>> builder.has_measurement(1 + 2j) + True + >>> builder.has_measurement(1 + 3j) + False + """ ``` - + ```python -# stimflow.ChunkBuilder.lookup_mids +# stimflow.ChunkBuilder.lookup_measurement_indices # (in class stimflow.ChunkBuilder) -def lookup_mids( +def lookup_measurement_indices( self, keys: Iterable[Any], *, - ignore_unmatched: bool = False, + ignore_unknown_measurements: bool = False, ) -> list[int]: """Looks up measurement indices by key. - Measurement keys are created automatically when appending measurement operations into the - circuit via the builder's append method. They are also created manually by methods like - `builder.record_measurement_group`. + Measurement keys are created automatically by the `append` method when appending + measurement operations (optionally tweaked by the `measure_key_func` argument). Args: keys: The measurement keys to lookup. - ignore_unmatched: Defaults to False. If set to True, keys that don't correspond + ignore_unknown_measurements: Defaults to False. If set to True, keys that don't correspond to measurements are ignored instead of raising an error. Returns: A list of offsets indicating when the measurements occurred. - """ -``` - -```python -# stimflow.ChunkBuilder.record_measurement_group + Examples: + >>> import stimflow as sf + >>> builder = sf.ChunkBuilder() + >>> builder.append("M", [1 + 2j]) + >>> builder.append("MX", [2j, 3j], measure_key_func=lambda e: str(e) + "test") -# (in class stimflow.ChunkBuilder) -def record_measurement_group( - self, - sub_keys: Iterable[Any], - *, - key: Any, -) -> None: - """Combines multiple measurement keys into one key. + >>> builder.lookup_measurement_indices([1 + 2j]) + [0] + >>> builder.lookup_measurement_indices(["2jtest"]) + [1] + >>> builder.lookup_measurement_indices(["2jtest", 1 + 2j]) + [0, 1] - Args: - sub_keys: The measurement keys to combine. - key: Where to store the combined result. + >>> builder.append("MZZ", [(0, 1)]) + >>> builder.lookup_measurement_indices([(1, 0)]) + [3] """ ``` diff --git a/glue/stimflow/doc/getting_started.ipynb b/glue/stimflow/doc/getting_started.ipynb index a76d8f99..ababa9de 100644 --- a/glue/stimflow/doc/getting_started.ipynb +++ b/glue/stimflow/doc/getting_started.ipynb @@ -289,8 +289,8 @@ "\n", " # Annotate the expected flows implemented by the circuit.\n", " for tile in code.tiles:\n", - " builder.add_flow(start=tile, ms=[tile.measure_qubit])\n", - " builder.add_flow(end=tile, ms=[tile.measure_qubit])\n", + " builder.add_flow(start=tile, measurements=[tile.measure_qubit])\n", + " builder.add_flow(end=tile, measurements=[tile.measure_qubit])\n", " for obs in code.flat_logicals:\n", " builder.add_flow(start=obs, end=obs)\n", "\n", @@ -1294,7 +1294,7 @@ " transversal_init = make_surface_code_init_chunk(code, \"X\")\n", " idle = make_surface_code_idle_chunk(code)\n", " transversal_measure = transversal_init.time_reversed()\n", - " \n", + "\n", " compiler.append(transversal_init)\n", " compiler.append(idle)\n", " compiler.append(idle)\n", diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py index 20581091..0ba48592 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py @@ -38,8 +38,8 @@ class ChunkBuilder: >>> builder.append("M", measure_qubits) >>> for m in measure_qubits: ... stabilizer = sf.PauliMap.from_zs([m-0.5, m+0.5]) - ... builder.add_flow(start=stabilizer, ms=[m]) - ... builder.add_flow(end=stabilizer, ms=[m]) + ... builder.add_flow(start=stabilizer, measurements=[m]) + ... builder.add_flow(end=stabilizer, measurements=[m]) >>> obs = sf.PauliMap({data_qubits[0]: "Z"}).with_obs_name("LZ") >>> builder.add_flow(start=obs, end=obs) >>> chunk = builder.finish_chunk() @@ -99,10 +99,11 @@ def __init__( allowed_qubits: Defaults to None (everything allowed). Specifies the qubit positions that the circuit is permitted to contain. - >>> import stimflow as sf - >>> data_qubits = range(5) - >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] - >>> builder = sf.ChunkBuilder(allowed_qubits=[*data_qubits, *measure_qubits]) + Examples: + >>> import stimflow as sf + >>> data_qubits = range(5) + >>> measure_qubits = [q + 0.5 for q in data_qubits[::-1]] + >>> builder = sf.ChunkBuilder(allowed_qubits=[*data_qubits, *measure_qubits]) """ self.allowed_qubits: set[complex] | None = None if allowed_qubits is None else set(allowed_qubits) self._num_measurements: int = 0 @@ -122,11 +123,11 @@ def __init__( for i, q in enumerate(sorted_complex(allowed_qubits)): self.q2i[q] = i - def _ensure_obs_index_of(self, name: Any) -> int: - result = self.o2i.get(name) + def _ensure_obs_index_of(self, obs_name: Any) -> int: + result = self.o2i.get(obs_name) if result is None: result = max(self.o2i.values(), default=-1) + 1 # TODO: avoid quadratic overhead - self.o2i[name] = result + self.o2i[obs_name] = result return result def _ensure_indices( @@ -183,32 +184,56 @@ def _rec(self, key: Any, value: list[int]) -> None: ) self._recorded_measurements[key] = value - def record_measurement_group(self, sub_keys: Iterable[Any], *, key: Any) -> None: - """Combines multiple measurement keys into one key. + def has_measurement(self, key: Any) -> bool: + """Determines if a measurement with the given key has been performed. Args: - sub_keys: The measurement keys to combine. - key: Where to store the combined result. - """ - self._rec(key, self.lookup_mids(sub_keys)) + key: The measurement key. - def has_measurement(self, key: Any) -> bool: + Returns: + Whether a measurement with the given key has been performed. + + Examples: + >>> import stimflow as sf + >>> builder = sf.ChunkBuilder() + >>> builder.append("M", [1 + 2j]) + >>> builder.has_measurement(1 + 2j) + True + >>> builder.has_measurement(1 + 3j) + False + """ return key in self._recorded_measurements - def lookup_mids(self, keys: Iterable[Any], *, ignore_unmatched: bool = False) -> list[int]: + def lookup_measurement_indices(self, keys: Iterable[Any], *, ignore_unknown_measurements: bool = False) -> list[int]: """Looks up measurement indices by key. - Measurement keys are created automatically when appending measurement operations into the - circuit via the builder's append method. They are also created manually by methods like - `builder.record_measurement_group`. + Measurement keys are created automatically by the `append` method when appending + measurement operations (optionally tweaked by the `measure_key_func` argument). Args: keys: The measurement keys to lookup. - ignore_unmatched: Defaults to False. If set to True, keys that don't correspond + ignore_unknown_measurements: Defaults to False. If set to True, keys that don't correspond to measurements are ignored instead of raising an error. Returns: A list of offsets indicating when the measurements occurred. + + Examples: + >>> import stimflow as sf + >>> builder = sf.ChunkBuilder() + >>> builder.append("M", [1 + 2j]) + >>> builder.append("MX", [2j, 3j], measure_key_func=lambda e: str(e) + "test") + + >>> builder.lookup_measurement_indices([1 + 2j]) + [0] + >>> builder.lookup_measurement_indices(["2jtest"]) + [1] + >>> builder.lookup_measurement_indices(["2jtest", 1 + 2j]) + [0, 1] + + >>> builder.append("MZZ", [(0, 1)]) + >>> builder.lookup_measurement_indices([(1, 0)]) + [3] """ result: list[int] = [] missing: list[Any] = [] @@ -223,7 +248,7 @@ def lookup_mids(self, keys: Iterable[Any], *, ignore_unmatched: bool = False) -> missing.append(key) else: result.extend(recs) - if missing and not ignore_unmatched: + if missing and not ignore_unknown_measurements: raise ValueError( "Some of the given measurement record keys don't exist.\n" f"Unmatched keys: {missing!r}\n" @@ -232,11 +257,129 @@ def lookup_mids(self, keys: Iterable[Any], *, ignore_unmatched: bool = False) -> return xor_sorted(result) def add_discarded_flow_input(self, flow: PauliMap | Tile) -> None: + """Annotates that an input stabilizer won't be used. + + When compiling chunks, it is normally an error if the output flows of one + chunk don't match up with the input flows of the next. For example, a + Z basis transversal measurement can't measure the X stabilizers of a code, + so a chunk performing can't declare flows with X basis inputs. But this + would cause an error during compilation, due the prior idling chunk having + X basis output flows. Adding the X basis stabilizers as discarded flow inputs + of the transversal chunk explicitly indicates that it is expected for this + mismatch to occur, so that no error is raised. + + Example: + >>> import stimflow as sf + >>> xx = sf.PauliMap.from_xs([0, 1]) + >>> zz = sf.PauliMap.from_zs([0, 1]) + + >>> init_builder = sf.ChunkBuilder() + >>> init_builder.append("R", [0, 1]) + >>> init_builder.add_flow(end=zz) + >>> init_builder.add_discarded_flow_output(sf.PauliMap.from_xs([0, 1])) + >>> init_chunk = init_builder.finish_chunk() + >>> init_chunk.verify() + + >>> idle_builder = sf.ChunkBuilder() + >>> idle_builder.append("MXX", [(0, 1)], measure_key_func=lambda e: ('X', e)) + >>> idle_builder.append("TICK") + >>> idle_builder.append("MZZ", [(0, 1)], measure_key_func=lambda e: ('Z', e)) + >>> idle_builder.add_flow(start=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(start=zz, measurements=[('Z', (0, 1))]) + >>> idle_builder.add_flow(end=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(end=zz, measurements=[('Z', (0, 1))]) + >>> idle_chunk = idle_builder.finish_chunk() + >>> idle_chunk.verify() + + >>> end_builder = sf.ChunkBuilder() + >>> end_builder.append("M", [0, 1]) + >>> end_builder.add_flow(start=zz, measurements=[0, 1]) + >>> end_builder.add_discarded_flow_input(sf.PauliMap.from_xs([0, 1])) + >>> end_chunk = end_builder.finish_chunk() + >>> end_chunk.verify() + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(init_chunk) + >>> compiler.append(idle_chunk) + >>> compiler.append(end_chunk) + >>> print(compiler.finish_circuit()) + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + R 0 1 + TICK + MXX 0 1 + TICK + MZZ 0 1 + DETECTOR(0.5, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + M 0 1 + DETECTOR(0.5, 0, 0) rec[-3] rec[-2] rec[-1] + """ if isinstance(flow, Tile): flow = flow.to_pauli_map() self._discarded_input_flows.append(flow) def add_discarded_flow_output(self, flow: PauliMap | Tile) -> None: + """Annotates that an output stabilizer won't be used. + + When compiling chunks, it is normally an error if the output flows of one + chunk don't match up with the input flows of the next. For example, a + Z basis transversal preparation can't prepare the X stabilizers of a code, + so a chunk performing can't declare flows with X basis inputs. But this + would cause an error during compilation, due the next idling chunk having + X basis input flows. Adding the X basis stabilizers as discarded flow outputs + of the transversal chunk explicitly indicates that it is expected for this + mismatch to occur, so that no error is raised. + + Example: + >>> import stimflow as sf + >>> xx = sf.PauliMap.from_xs([0, 1]) + >>> zz = sf.PauliMap.from_zs([0, 1]) + + >>> init_builder = sf.ChunkBuilder() + >>> init_builder.append("R", [0, 1]) + >>> init_builder.add_flow(end=zz) + >>> init_builder.add_discarded_flow_output(sf.PauliMap.from_xs([0, 1])) + >>> init_chunk = init_builder.finish_chunk() + >>> init_chunk.verify() + + >>> idle_builder = sf.ChunkBuilder() + >>> idle_builder.append("MXX", [(0, 1)], measure_key_func=lambda e: ('X', e)) + >>> idle_builder.append("TICK") + >>> idle_builder.append("MZZ", [(0, 1)], measure_key_func=lambda e: ('Z', e)) + >>> idle_builder.add_flow(start=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(start=zz, measurements=[('Z', (0, 1))]) + >>> idle_builder.add_flow(end=xx, measurements=[('X', (0, 1))]) + >>> idle_builder.add_flow(end=zz, measurements=[('Z', (0, 1))]) + >>> idle_chunk = idle_builder.finish_chunk() + >>> idle_chunk.verify() + + >>> end_builder = sf.ChunkBuilder() + >>> end_builder.append("M", [0, 1]) + >>> end_builder.add_flow(start=zz, measurements=[0, 1]) + >>> end_builder.add_discarded_flow_input(sf.PauliMap.from_xs([0, 1])) + >>> end_chunk = end_builder.finish_chunk() + >>> end_chunk.verify() + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(init_chunk) + >>> compiler.append(idle_chunk) + >>> compiler.append(end_chunk) + >>> print(compiler.finish_circuit()) + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(1, 0) 1 + R 0 1 + TICK + MXX 0 1 + TICK + MZZ 0 1 + DETECTOR(0.5, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + M 0 1 + DETECTOR(0.5, 0, 0) rec[-3] rec[-2] rec[-1] + """ if isinstance(flow, Tile): flow = flow.to_pauli_map() self._discarded_output_flows.append(flow) @@ -246,8 +389,8 @@ def add_flow( *, start: PauliMap | Tile | Literal["auto"] | None = None, end: PauliMap | Tile | Literal["auto"] | None = None, - ms: Iterable[Any] | Literal["auto"] = (), - ignore_unmatched_ms: bool = False, + measurements: Iterable[Any] | Literal["auto"] = (), + ignore_unknown_measurements: bool = False, center: complex | None = None, flags: Iterable[str] = frozenset(), sign: bool | None = None, @@ -263,15 +406,15 @@ def add_flow( end: Defaults to None (empty). The stabilizer that the flow ends as, at the end of the circuit. If the flow ends within the circuit, this should be set to None or an empty PauliMap. - ms: Defaults to empty. The keys identifying measurements mediate the flow. + measurements: Defaults to empty. The keys identifying measurements mediate the flow. For example, if a stabilizer is measured by a circuit then this would typically be a singleton list containing the measurement that reveals the stabilizer's value. - ignore_unmatched_ms: Defaults to False. When set to False, unrecognized measurement + ignore_unknown_measurements: Defaults to False. When set to False, unrecognized measurement ids cause the method to raise an exception instead of adding the flow. When set to True, unrecognized measurements are silently discarded. center: Defaults to None (unused). Optional metadata specifying coordinates for the - flow. Typically these coordinates will end up being exposed as the parens args + flow. Typically, these coordinates will end up being exposed as the parens args on the DETECTOR instruction created when producing a stim circuit. When not specified, the coordinates will instead be inferred in some heuristic way. flags: Defaults to empty. Hashable equatable values associated with the flow. When @@ -290,17 +433,17 @@ def add_flow( >>> builder.append('TICK') >>> builder.append('CX', [(1j, 0)]) - >>> builder.add_flow(end=sf.PauliMap.from_xs([0, 1j]), ms=[1j]) + >>> builder.add_flow(end=sf.PauliMap.from_xs([0, 1j]), measurements=[1j]) >>> builder.add_flow(end=sf.PauliMap.from_zs([0, 1j])) - >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), ms=[1j]) + >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), measurements=[1j]) >>> builder.finish_chunk().verify() """ - auto_count = (start == "auto") + (end == "auto") + (ms == "auto") + auto_count = (start == "auto") + (end == "auto") + (measurements == "auto") if auto_count > 1: raise ValueError("Only one of `start`, `end`, and `ms` can be set to auto.\n" f" {start=}" - f" {ms=}" + f" {measurements=}" f" {end=}") if isinstance(start, PauliMap): obs_name = start.obs_name @@ -315,15 +458,15 @@ def add_flow( elif end == "auto": out = self._flows_with_auto_end end = PauliMap(obs_name=obs_name) - elif ms == "auto": + elif measurements == "auto": out = self._flows_with_auto_ms - ms = () + measurements = () out.append( Flow( start=start, end=end, - measurement_indices=self.lookup_mids(ms, ignore_unmatched=ignore_unmatched_ms), + measurement_indices=self.lookup_measurement_indices(measurements, ignore_unknown_measurements=ignore_unknown_measurements), center=center, flags=flags, sign=sign, @@ -483,7 +626,7 @@ def append( stored in the measurement tracker keyed by the position of the qubit being measured (or by a custom key, if `measure_key_func` is specified). The indices of the measurements can be looked up later via - `builder.lookup_mids([key1, key2, ...])`. + `builder.lookup_measurement_indices([key1, key2, ...])`. Args: gate: The name of the gate to append, such as "H" or "M" or "CX". @@ -501,7 +644,7 @@ def append( qubit multiple times. This function can transform that position into a different value (for example, you might set `measure_key_func=lambda pos: (pos, 'first_cycle')` for - measurements during the first cycle of the circuit. + measurements during the first cycle of the circuit). tag: Defaults to "" (no tag). A custom tag to attach to the instruction(s) appended into the stim circuit. unknown_qubit_append_mode: Defaults to 'auto'. The available options are: @@ -577,7 +720,7 @@ def append( ) else: t0 = self._num_measurements - times = self.lookup_mids(targets) + times = self.lookup_measurement_indices(targets) rec_targets = [stim.target_rec(t - t0) for t in sorted(times)] self.circuit.append(data.name, rec_targets, arg, tag=tag) @@ -813,7 +956,7 @@ def append_feedback( indices.append(i) indices = sorted(indices) t0 = self._num_measurements - times = self.lookup_mids(control_keys) + times = self.lookup_measurement_indices(control_keys) rec_targets = [stim.target_rec(t - t0) for t in sorted(times)] for rec in rec_targets: for i in indices: diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py index a2c56f89..968a1e16 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder_test.py @@ -39,28 +39,28 @@ def test_append_measurements(): builder = stimflow.ChunkBuilder(range(6)) builder.append("MXX", [(2, 3)]) - assert builder.lookup_mids([(2, 3)]) == [0] - assert builder.lookup_mids([(3, 2)]) == [0] + assert builder.lookup_measurement_indices([(2, 3)]) == [0] + assert builder.lookup_measurement_indices([(3, 2)]) == [0] builder.append("MYY", [(5, 4)]) - assert builder.lookup_mids([(4, 5)]) == [1] - assert builder.lookup_mids([(5, 4)]) == [1] + assert builder.lookup_measurement_indices([(4, 5)]) == [1] + assert builder.lookup_measurement_indices([(5, 4)]) == [1] builder.append("M", [3]) - assert builder.lookup_mids([3]) == [2] + assert builder.lookup_measurement_indices([3]) == [2] def test_append_measurements_canonical_order(): builder = stimflow.ChunkBuilder(range(6)) builder.append("MX", [5, 2, 3]) - assert builder.lookup_mids([2]) == [0] - assert builder.lookup_mids([3]) == [1] - assert builder.lookup_mids([5]) == [2] + assert builder.lookup_measurement_indices([2]) == [0] + assert builder.lookup_measurement_indices([3]) == [1] + assert builder.lookup_measurement_indices([5]) == [2] builder.append("MZZ", [(5, 2), (3, 4)]) - assert builder.lookup_mids([(2, 5)]) == [3] - assert builder.lookup_mids([(3, 4)]) == [4] + assert builder.lookup_measurement_indices([(2, 5)]) == [3] + assert builder.lookup_measurement_indices([(3, 4)]) == [4] assert builder.circuit == stim.Circuit( """ @@ -76,8 +76,8 @@ def test_append_mpp(): xxx = stimflow.PauliMap.from_xs([2 + 3j, 5 + 7j, 11 + 13j]) z_z = stimflow.PauliMap.from_zs([11 + 13j, 2 + 3j]) builder.append("MPP", [xxx, z_z]) - assert builder.lookup_mids([xxx]) == [0] - assert builder.lookup_mids([z_z]) == [1] + assert builder.lookup_measurement_indices([xxx]) == [0] + assert builder.lookup_measurement_indices([z_z]) == [1] assert builder.circuit == stim.Circuit( """ @@ -199,9 +199,9 @@ def test_skip_unknown_1qm(): M 1 2 3 """ ) - assert builder.lookup_mids([1]) == [0] - assert builder.lookup_mids([2]) == [1] - assert builder.lookup_mids([3]) == [2] + assert builder.lookup_measurement_indices([1]) == [0] + assert builder.lookup_measurement_indices([2]) == [1] + assert builder.lookup_measurement_indices([3]) == [2] def test_skip_unknown_2qm(): @@ -213,8 +213,8 @@ def test_skip_unknown_2qm(): MZZ 0 1 2 3 """ ) - assert builder.lookup_mids([(0, 1)]) == builder.lookup_mids([(1, 0)]) == [0] - assert builder.lookup_mids([(2, 3)]) == builder.lookup_mids([(3, 2)]) == [1] + assert builder.lookup_measurement_indices([(0, 1)]) == builder.lookup_measurement_indices([(1, 0)]) == [0] + assert builder.lookup_measurement_indices([(2, 3)]) == builder.lookup_measurement_indices([(3, 2)]) == [1] def test_partial_observable_include_memory_experiment(): @@ -239,12 +239,12 @@ def test_partial_observable_include_memory_experiment(): builder_bulk.append("MPP", [stab_z1]) builder_bulk.append("MPP", [stab_x]) - builder_bulk.add_flow(start=stab_z0, ms=[stab_z0]) - builder_bulk.add_flow(start=stab_z1, ms=[stab_z1]) - builder_bulk.add_flow(start=stab_x, ms=[stab_x]) - builder_bulk.add_flow(end=stab_z0, ms=[stab_z0]) - builder_bulk.add_flow(end=stab_z1, ms=[stab_z1]) - builder_bulk.add_flow(end=stab_x, ms=[stab_x]) + builder_bulk.add_flow(start=stab_z0, measurements=[stab_z0]) + builder_bulk.add_flow(start=stab_z1, measurements=[stab_z1]) + builder_bulk.add_flow(start=stab_x, measurements=[stab_x]) + builder_bulk.add_flow(end=stab_z0, measurements=[stab_z0]) + builder_bulk.add_flow(end=stab_z1, measurements=[stab_z1]) + builder_bulk.add_flow(end=stab_x, measurements=[stab_x]) builder_bulk.add_flow(start=obs_x, end=obs_x) builder_bulk.add_flow(start=obs_z, end=obs_z) chunk_bulk = builder_bulk.finish_chunk() @@ -255,8 +255,8 @@ def test_partial_observable_include_memory_experiment(): builder_end.add_discarded_flow_input(stab_z0) builder_end.add_discarded_flow_input(stab_z1) builder_end.add_flow(start=obs_z) - builder_end.add_flow(start=stab_x, ms=stab_x.keys()) - builder_end.add_flow(start=obs_x, ms=obs_x.keys()) + builder_end.add_flow(start=stab_x, measurements=stab_x.keys()) + builder_end.add_flow(start=obs_x, measurements=obs_x.keys()) chunk_end = builder_end.finish_chunk() chunk_init.verify() diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py index 14a74308..33d2b10d 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py @@ -72,13 +72,13 @@ def __str__(self) -> str: lines.append(" det_flows {") for key, flow in self.open_flows.items(): if isinstance(flow, Flow) and flow.obs_name is None: - lines.append(f" {flow.end}, ms={flow.measurement_indices}") + lines.append(f" {flow.end}, measurements={flow.measurement_indices}") lines.append(" }") lines.append(" obs_flows {") for key, flow in self.open_flows.items(): if isinstance(flow, Flow) and flow.obs_name is not None: - lines.append(f" {flow.key_end}: ms={flow.measurement_indices}") + lines.append(f" {flow.key_end}: measurements={flow.measurement_indices}") lines.append(" }") lines.append(f" num_measurements = {self.num_measurements}") diff --git a/glue/stimflow/src/stimflow/_chunk/_code_util.py b/glue/stimflow/src/stimflow/_chunk/_code_util.py index f558c54e..924778dc 100644 --- a/glue/stimflow/src/stimflow/_chunk/_code_util.py +++ b/glue/stimflow/src/stimflow/_chunk/_code_util.py @@ -152,7 +152,7 @@ def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: if end is None: prev_builder.add_discarded_flow_input(tile) else: - prev_builder.add_flow(start=tile, end=end, ms=start.keys() - end.keys()) + prev_builder.add_flow(start=tile, end=end, measurements=start.keys() - end.keys()) for k, obs in enumerate(prev_code.flat_logicals): assert obs.obs_name is not None end = clipped(obs, measured) @@ -160,7 +160,7 @@ def clipped(original: PauliMap, dissipated: PauliMap) -> PauliMap | None: prev_builder.add_discarded_flow_input(obs) else: prev_key2obs[obs.obs_name] = end - prev_builder.add_flow(start=obs, end=end, ms=obs.keys() - end.keys()) + prev_builder.add_flow(start=obs, end=end, measurements=obs.keys() - end.keys()) next_builder = ChunkBuilder(next_code.data_set) next_obs2key = {} diff --git a/glue/stimflow/src/stimflow/_chunk/_flow_util.py b/glue/stimflow/src/stimflow/_chunk/_flow_util.py index ba6e9e0f..96bb3f83 100644 --- a/glue/stimflow/src/stimflow/_chunk/_flow_util.py +++ b/glue/stimflow/src/stimflow/_chunk/_flow_util.py @@ -23,7 +23,7 @@ def _solve_auto_flow_starts( new_flows = [] for flow in flows: - stim_end = cast(PauliMap, flow.end).to_stim_pauli_string(q2i, num_qubits=num_qubits) + stim_end = flow.end.to_stim_pauli_string(q2i, num_qubits=num_qubits) try: stim_start = stim_end.before(circuit) except ValueError: @@ -48,7 +48,7 @@ def _solve_auto_flow_ends( new_flows = [] for flow in flows: - stim_start = cast(PauliMap, flow.start).to_stim_pauli_string(q2i, num_qubits=num_qubits) + stim_start = flow.start.to_stim_pauli_string(q2i, num_qubits=num_qubits) try: stim_end = stim_start.after(circuit) except ValueError: diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py index 97fb0c19..a815d634 100644 --- a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py @@ -650,8 +650,8 @@ def make_phenom_circuit( builder.append( "MPP", [tile], measure_key_func=lambda _: f"det{k1},{k2}", arg=noise.flip_result ) - builder.add_flow(end=tile, ms=[f"det{k1},{k2}"]) - builder.add_flow(start=tile, ms=[f"det{k1},{k2}"]) + builder.add_flow(end=tile, measurements=[f"det{k1},{k2}"]) + builder.add_flow(start=tile, measurements=[f"det{k1},{k2}"]) measure_chunk = builder.finish_chunk()