diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 040221ad..219fbc39 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 --suppress_examples_warning_for stimflow.str_svg stimflow.str_html stimflow.Viewable3dModelGLTF test_sinter: runs-on: ubuntu-24.04 steps: 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/dev/doctest_proper.py b/dev/doctest_proper.py index 4557ec0a..de9fb839 100755 --- a/dev/doctest_proper.py +++ b/dev/doctest_proper.py @@ -25,6 +25,9 @@ '__doc__', '__loader__', '__file__', + '__firstlineno__', + '__static_attributes__', + '__match_args__', } @@ -79,6 +82,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 +105,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/README.md b/glue/stimflow/README.md new file mode 100644 index 00000000..0aa0e29b --- /dev/null +++ b/glue/stimflow/README.md @@ -0,0 +1,139 @@ +stimflow: annealed utilities for creating QEC circuits +================================================= + +stimflow is a library for creating quantum error correction circuits. + +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. + +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 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 + +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, 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) + + 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() +``` diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md new file mode 100644 index 00000000..c1fdca60 --- /dev/null +++ b/glue/stimflow/doc/api.md @@ -0,0 +1,4425 @@ +# 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`](#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) + - [`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_measurement_indices`](#stimflow.ChunkBuilder.lookup_measurement_indices) +- [`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`](#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) + - [`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.fused_with_next_flow`](#stimflow.Flow.fused_with_next_flow) + - [`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) + - [`stimflow.Flow.with_xz_flipped`](#stimflow.Flow.with_xz_flipped) +- [`stimflow.FlowMetadata`](#stimflow.FlowMetadata) + - [`stimflow.FlowMetadata.__init__`](#stimflow.FlowMetadata.__init__) +- [`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.LineDataFor3DModel`](#stimflow.LineDataFor3DModel) + - [`stimflow.LineDataFor3DModel.__init__`](#stimflow.LineDataFor3DModel.__init__) + - [`stimflow.LineDataFor3DModel.fused`](#stimflow.LineDataFor3DModel.fused) +- [`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_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) +- [`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`](#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) + - [`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.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) + - [`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.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) +- [`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.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`](#stimflow.verify_distance_is_at_least) +- [`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 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() + """ +``` + + +```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, +): + """Creates a `stimflow.Chunk` with the given values. + + 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. 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. 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() + """ +``` + + +```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 + +# (in class stimflow.Chunk) +def verify_distance_is_at_least( + self, + minimum_distance: int, + *, + noise: float | NoiseModel = 0.001, +): + """Verifies undetected logical errors require at least the given number of physical errors. + + 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_obs_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, 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) + """ +``` + + +```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: + """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, 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() + + >>> 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 + """ +``` + + +```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. + + 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]) + """ +``` + + +```python +# stimflow.ChunkBuilder.add_discarded_flow_input + +# (in class stimflow.ChunkBuilder) +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] + """ +``` + + +```python +# stimflow.ChunkBuilder.add_discarded_flow_output + +# (in class stimflow.ChunkBuilder) +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] + """ +``` + + +```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, + measurements: "Iterable[Any] | Literal['auto']" = (), + ignore_unknown_measurements: bool = False, + 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. + 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_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 + 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]), measurements=[1j]) + >>> builder.add_flow(end=sf.PauliMap.from_zs([0, 1j])) + >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), measurements=[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_measurement_indices([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: + """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_measurement_indices + +# (in class stimflow.ChunkBuilder) +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 by the `append` method when appending + measurement operations (optionally tweaked by the `measure_key_func` argument). + + Args: + keys: The measurement keys to lookup. + 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] + """ +``` + + +```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 + +# (in class stimflow.ChunkLoop) +def verify_distance_is_at_least( + self, + minimum_distance: int, + *, + noise: float | NoiseModel = 0.001, +): + """Verifies undetected logical errors require at least the given number of physical errors. + + 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. + + (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 + 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, + measurement_indices: Iterable[int] = (), + 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). + 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_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. + 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.fused_with_next_flow + +# (in class stimflow.Flow) +def fused_with_next_flow( + self, + next_flow: Flow, + *, + next_flow_measure_offset: int, +) -> Flow: +``` + + +```python +# stimflow.Flow.obs_name + +# (in class stimflow.Flow) +@property +def obs_name( + 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] = , + center: complex | None = , + 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.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 `LayerReset`, then a `LayerRotation`, + then a few `LayerInteract`s, then a `LayerMeasure`. + """ + 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.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 + +# (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, + allow_multiple_uses_of_a_qubit_in_one_tick: 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. + + 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. + """ +``` + + +```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: + """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 + """ +``` + + +```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, + *, + 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. + 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. + + 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"}, obs_name="test")) + (obs_name='test') X0*Y1*Z2 + """ +``` + + +```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_obs_name + +# (in class stimflow.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. + """ +``` + + +```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.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 + +# (in class stimflow.StabilizerCode) +def verify_distance_is_at_least( + self, + minimum_distance: int, +): +``` + + +```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.TextDataFor3DModel + +# (at top-level in the stimflow module) +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.TextDataFor3DModel.__init__ + +# (in class stimflow.TextDataFor3DModel) +def __init__( + self, + *, + text: str, + 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. + + 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; 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 height of the rectangle is determined + automatically from the text. + Should be perpendicular to `forward`. + 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). + """ +``` + + +```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.TriangleDataFor3DModel + +# (at top-level in the stimflow module) +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.TriangleDataFor3DModel.__init__ + +# (in class stimflow.TriangleDataFor3DModel) +def __init__( + self, + *, + rgba: tuple[float, float, float, float], + triangle_list: np.ndarray | Iterable[Sequence[Sequence[float]]], +): + """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). + + 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.TriangleDataFor3DModel.fused + +# (in class stimflow.TriangleDataFor3DModel) +def fused( + data: Iterable[TriangleDataFor3DModel], +) -> list[TriangleDataFor3DModel]: + """Attempts to combine triangle data instances into fewer instances. + """ +``` + + +```python +# stimflow.TriangleDataFor3DModel.rect + +# (in class stimflow.TriangleDataFor3DModel) +def rect( + *, + rgba: tuple[float, float, float, float], + origin: Iterable[float], + d1: Iterable[float], + d2: Iterable[float], +) -> TriangleDataFor3DModel: + """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_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.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[TriangleDataFor3DModel | LineDataFor3DModel | TextDataFor3DModel], +) -> Viewable3dModelGLTF: + """Creates a 3d model containing the given elements. + + Args: + elements: A list of objects to include in the model. The list can include triangles + (TriangleDataFor3DModel), lines (LineDataFor3DModel), and text (TextDataFor3DModel). + + 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. + + 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] + "...") + + + + + + + +```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 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 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) + """ +``` + + +```python +# stimflow.sorted_complex + +# (at top-level in the stimflow module) +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)] + """ +``` + + +```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 + +# (at top-level in the stimflow module) +def verify_distance_is_at_least( + obj: stim.Circuit | stim.DetectorErrorModel | StabilizerCode, + minimum_distance: int, +): +``` + + +```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. + + 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/doc/getting_started.ipynb b/glue/stimflow/doc/getting_started.ipynb new file mode 100644 index 00000000..ababa9de --- /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, 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", + " 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..9a9542f1 --- /dev/null +++ b/glue/stimflow/setup.py @@ -0,0 +1,40 @@ +# 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() +with open('requirements.txt', encoding='UTF-8') as f: + requirements = f.read().splitlines() + +__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', '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 new file mode 100644 index 00000000..8f15097a --- /dev/null +++ b/glue/stimflow/src/stimflow/__init__.py @@ -0,0 +1,52 @@ +__version__ = "0.1.0" + +from stimflow._chunk import ( + Chunk, + ChunkBuilder, + ChunkCompiler, + ChunkInterface, + ChunkLoop, + ChunkReflow, + find_d1_error, + find_d2_error, + FlowMetadata, + Patch, + StabilizerCode, + StimCircuitLoom, + transversal_code_transition_chunks, + verify_distance_is_at_least, +) +from stimflow._core import ( + append_reindexed_content_to_circuit, + circuit_to_dem_target_measurement_records_map, + circuit_with_xz_flipped, + 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 ( + LayerCircuit, + transpile_to_z_basis_interaction_circuit, +) +from stimflow._viz import ( + LineDataFor3DModel, + TriangleDataFor3DModel, + Viewable3dModelGLTF, + html_viewer_for_gltf_model, + make_3d_model, + stim_circuit_html_viewer, + svg, + TextDataFor3DModel, +) diff --git a/glue/stimflow/src/stimflow/_chunk/__init__.py b/glue/stimflow/src/stimflow/_chunk/__init__.py new file mode 100644 index 00000000..b42690d4 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/__init__.py @@ -0,0 +1,18 @@ +"""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 +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 ( + find_d1_error, + find_d2_error, + transversal_code_transition_chunks, + verify_distance_is_at_least, +) +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..e9fa1915 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk.py @@ -0,0 +1,923 @@ +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, +) +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 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, + 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, + ): + """Creates a `stimflow.Chunk` with the given values. + + 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. 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. 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()} + 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_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 + 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_name=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_name=out.obs_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_name is not None: + compiler.o2i.setdefault(flow.obs_name, 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_name=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]) + if target.is_logical_observable_id(): + 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) + + flows.append( + Flow( + start=start_pm, + end=end_pm, + measurement_indices=[m - record_range.start for m in records[target] if m in record_range], + center=center, + ) + ) + + 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_name={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 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(self, minimum_distance: int, *, noise: float | NoiseModel = 1e-3): + """Verifies undetected logical errors require at least the given number of physical errors. + + 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_obs_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, 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) + """ + __tracebackhide__ = True + circuit = self.to_closed_circuit() + if isinstance(noise, float): + 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(circuit, minimum_distance) + + 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, allow_multiple_uses_of_a_qubit_in_one_tick=True) + 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.items(): + inp[self.q2i[q]] = p + 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)) + ) + 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, + measurement_indices=[m + nm for m in rev_flow.measurements_copy()], + flags=flow.flags, + ) + 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_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_name 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_name 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_name 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..0ba48592 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_builder.py @@ -0,0 +1,963 @@ +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: + """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, 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() + + >>> 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__( + 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. + + 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 + 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, 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[obs_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 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 + """ + return key in self._recorded_measurements + + 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 by the `append` method when appending + measurement operations (optionally tweaked by the `measure_key_func` argument). + + Args: + keys: The measurement keys to lookup. + 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] = [] + 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_unknown_measurements: + 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: + """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) + + def add_flow( + self, + *, + start: PauliMap | Tile | Literal["auto"] | None = None, + end: PauliMap | Tile | Literal["auto"] | None = None, + measurements: Iterable[Any] | Literal["auto"] = (), + ignore_unknown_measurements: bool = False, + 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. + 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_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 + 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]), measurements=[1j]) + >>> builder.add_flow(end=sf.PauliMap.from_zs([0, 1j])) + >>> builder.add_flow(start=sf.PauliMap.from_xs([1j]), measurements=[1j]) + + >>> builder.finish_chunk().verify() + """ + 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" {measurements=}" + f" {end=}") + if isinstance(start, PauliMap): + obs_name = start.obs_name + elif isinstance(end, PauliMap): + obs_name = end.obs_name + else: + obs_name = None + out = self._flows + if start == "auto": + out = self._flows_with_auto_start + start = PauliMap(obs_name=obs_name) + elif end == "auto": + out = self._flows_with_auto_end + end = PauliMap(obs_name=obs_name) + elif measurements == "auto": + out = self._flows_with_auto_ms + measurements = () + + out.append( + Flow( + start=start, + end=end, + measurement_indices=self.lookup_measurement_indices(measurements, ignore_unknown_measurements=ignore_unknown_measurements), + 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_measurement_indices([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.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_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.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.obs_name) + if old_arg is None: + self.o2i[targets.obs_name] = int(arg) + elif old_arg != arg: + raise ValueError( + 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.obs_name) + + self._ensure_indices( + targets.keys(), + 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_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) + + 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_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: + 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..968a1e16 --- /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_measurement_indices([(2, 3)]) == [0] + assert builder.lookup_measurement_indices([(3, 2)]) == [0] + + builder.append("MYY", [(5, 4)]) + assert builder.lookup_measurement_indices([(4, 5)]) == [1] + assert builder.lookup_measurement_indices([(5, 4)]) == [1] + + builder.append("M", [3]) + 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_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_measurement_indices([(2, 5)]) == [3] + assert builder.lookup_measurement_indices([(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_measurement_indices([xxx]) == [0] + assert builder.lookup_measurement_indices([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_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) + ) + + 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_measurement_indices([1]) == [0] + assert builder.lookup_measurement_indices([2]) == [1] + assert builder.lookup_measurement_indices([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_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(): + 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]) + + 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, 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() + + 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, measurements=stab_x.keys()) + builder_end.add_flow(start=obs_x, measurements=obs_x.keys()) + 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..33d2b10d --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py @@ -0,0 +1,618 @@ +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_name is None: + 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}: measurements={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_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_name, int): + obs2i[obs_index] = next_obs_index + next_obs_index += 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 + + 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.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.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.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") + + 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, measurement_indices=[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.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) + 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).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 + 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 = flow.fused_with_next_flow( + Flow(start=obs_port), next_flow_measure_offset=0 + ) + 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) + + 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.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() + 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, + measurement_indices=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.obs_name is not None: + self.discarded_observables.add(ps.obs_name) + for ps in chunk.discarded_outputs: + 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 + + # 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.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_name, 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_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_name, 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.fused_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 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..01869839 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler_test.py @@ -0,0 +1,714 @@ +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]), measurement_indices=[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]).with_obs_name(0), center=0)], + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + MR 0 + """ + ), + flows=[ + stimflow.Flow( + 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, + ) + ], + ) + ) + compiler.append( + stimflow.Chunk( + circuit=stim.Circuit( + """ + QUBIT_COORDS(0, 0) 0 + M 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( + """ + 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]), 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 + ), + 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]), measurement_indices=[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]).with_obs_name(3), center=0)], + ) + ) + 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]).with_obs_name(3), + end=stimflow.PauliMap.from_zs([0]).with_obs_name(3), + measurement_indices=[0], + center=0, + ) + ], + ) + ], + 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]).with_obs_name(3), measurement_indices=[0], center=0)], + ) + ) + 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"}), measurement_indices=[0]), + stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), measurement_indices=[0]), + ], + ) + chunk3 = stimflow.Chunk( + circuit=stim.Circuit( + """ + MR 0 + """ + ), + q2i={0: 0}, + flows=[stimflow.Flow(center=0, start=stimflow.PauliMap({0: "Z"}), measurement_indices=[0])], + ) + + 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 + TICK + M 0 + DETECTOR(0, 0, 0) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1) rec[-2] rec[-1] + """ + ) + + 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 + TICK + M 0 + DETECTOR(0, 0, 0, 999) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1) rec[-2] rec[-1] + """ + ) + + 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 + TICK + M 0 + DETECTOR(0, 0, 0, 999) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1, 999) rec[-2] rec[-1] + """ + ) + + 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 + TICK + M 0 + DETECTOR(0, 0, 0) rec[-1] + TICK + MR 0 + DETECTOR(0, 0, 1, 999) rec[-2] rec[-1] + """ + ) + + 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 + 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, measurement_indices=[0]), + stimflow.Flow(end=zz, center=0, measurement_indices=[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, measurement_indices=[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.with_obs_name("a"), measurement_indices=[1]), + stimflow.Flow(end=xx.with_obs_name("b"), measurement_indices=[0]), + ], + ) + ) + + c.append( + stimflow.Chunk( + stim.Circuit( + """ + MPP X0*X1 + """ + ), + q2i={0: 0, 1: 1}, + discarded_inputs=[zz.with_obs_name("a")], + flows=[stimflow.Flow(start=xx.with_obs_name("b"), measurement_indices=[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(measurement_indices=[-1], center=0), + stimflow.Flow(measurement_indices=[-2], center=0), + stimflow.Flow(measurement_indices=[-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]), measurement_indices=[0]), + stimflow.Flow(end=stimflow.PauliMap.from_zs([0, 2]), measurement_indices=[-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(measurement_indices=[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(-1, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + REPEAT 3 { + R 1 + M 1 + DETECTOR(-1, 0, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + } + R 1 + M 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_interface.py b/glue/stimflow/src/stimflow/_chunk/_chunk_interface.py new file mode 100644 index 00000000..06462976 --- /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.obs_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.obs_name is None], + discards=[discard for discard in self.discards if discard.obs_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 + ) + + 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.obs_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.obs_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..deb77425 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_loop.py @@ -0,0 +1,175 @@ +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, +) +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(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, allow_multiple_uses_of_a_qubit_in_one_tick=True) + circuit = noise.noisy_circuit_skipping_mpp_boundaries(circuit) + verify_distance_is_at_least(circuit, minimum_distance) + + 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, allow_multiple_uses_of_a_qubit_in_one_tick=True) + 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..d27af7c5 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py @@ -0,0 +1,312 @@ +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. + + (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 + 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.obs_name == out.obs_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.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.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..8d67b562 --- /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_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_obs_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_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 new file mode 100644 index 00000000..d9960424 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_test.py @@ -0,0 +1,582 @@ +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({}), measurement_indices=[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, measurement_indices=[0], center=0), stimflow.Flow(end=zz, measurement_indices=[1], center=0)], + ) + chunk2 = stimflow.Chunk( + q2i={0: 0, 1: 1}, + circuit=stim.Circuit( + """ + MPP Y0*Y1 + """ + ), + flows=[stimflow.Flow(start=yy, measurement_indices=[0], center=0)], + discarded_inputs=[xx], + ) + reflow = stimflow.ChunkReflow({yy: [xx, zz], xx: [xx]}) + chunk1.verify() + chunk2.verify() + reflow.verify() + 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(): + 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]), + measurement_indices=(), + 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]), + measurement_indices=(0,), + 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]).with_obs_name(0), + end=stimflow.PauliMap.from_zs([1 + 2j]).with_obs_name(0), + measurement_indices=(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({}), measurement_indices=[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]}), 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]}).with_obs_name("X"), end=stimflow.PauliMap({"X": [0, 1]}).with_obs_name("X"), + ), + stimflow.Flow( + start=stimflow.PauliMap({"Z": [0, 1j]}).with_obs_name("Z"), end=stimflow.PauliMap({"Z": [0, 1j]}).with_obs_name("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_obs_name("L2"))], + o2i={"L2": 2}, + ) + chunk.verify() + + +def test_verify_distance(): + 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]) + 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, 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/_code_util.py b/glue/stimflow/src/stimflow/_chunk/_code_util.py new file mode 100644 index 00000000..924778dc --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_code_util.py @@ -0,0 +1,199 @@ +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 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(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() + 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}, + obs_name=original.obs_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, 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) + if end is None: + prev_builder.add_discarded_flow_input(obs) + else: + prev_key2obs[obs.obs_name] = end + prev_builder.add_flow(start=obs, end=end, measurements=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.obs_name is not None + start = clipped(obs, reset) + if start is None: + next_builder.add_discarded_flow_output(obs) + else: + 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) + reflow = ChunkReflow.from_auto_rewrite_transitions_using_stable( + stable=[ + cast(PauliMap, flow.start) + for flow in next_builder._flows + if flow.obs_name is None + if flow.start + ], + transitions=[ + (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) + 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..9b7d10a3 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_code_util_test.py @@ -0,0 +1,176 @@ +import pytest +import stim + +import stimflow + + +def test_verify_distance_is_at_least_23(): + stimflow.verify_distance_is_at_least( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + """ + ), + 2, + ) + + stimflow.verify_distance_is_at_least( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + DETECTOR rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ), + 2, + ) + + stimflow.verify_distance_is_at_least( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + DETECTOR rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ).detector_error_model(), + 2, + ) + + with pytest.raises(ValueError, match="distance 1 error"): + stimflow.verify_distance_is_at_least( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ), + 2, + ) + + with pytest.raises(ValueError, match="distance 1 error"): + stimflow.verify_distance_is_at_least( + stim.Circuit( + """ + R 0 + X_ERROR(0.125) 0 + M 0 + OBSERVABLE_INCLUDE(0) rec[-1] + """ + ), + 3, + ) + + 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( + 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( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=9, + rounds=3, + after_clifford_depolarization=1e-3, + ), + 2, + ) + + 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( + 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( + stim.Circuit.generated( + code_task="repetition_code:memory", + distance=2, + rounds=3, + after_clifford_depolarization=1e-3, + ), + 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_obs_name("X"), + stimflow.PauliMap.from_zs([0j + 1, 1j + 1, 2j + 1]).with_obs_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..96bb3f83 --- /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 = 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 = 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_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: + 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..9d2edfab --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_flow_util_test.py @@ -0,0 +1,162 @@ +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]}), measurement_indices=[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]).with_obs_name("L2"), + end=stimflow.PauliMap.from_zs([1, 2]).with_obs_name("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]).with_obs_name("L2"), + end=stimflow.PauliMap.from_zs([1, 2]).with_obs_name("L2"), + measurement_indices=[0], + ), + ] + ) + 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..05185d84 --- /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]}, 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")), + ], + ) + + +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..a815d634 --- /dev/null +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py @@ -0,0 +1,754 @@ +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.obs_name is None: + raise ValueError(f"Unnamed logical operator: {obs!r}") + 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: + """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_obs_name(f"inferred_X{k}"), z2.with_obs_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_obs_name((over_obs.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_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=}.") + + 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(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. + """ + __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, measurements=[f"det{k1},{k2}"]) + builder.add_flow(start=tile, measurements=[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..cf0828e0 --- /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_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_name 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_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_name 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_obs_name("LX"), + stimflow.PauliMap.from_zs([0, 2]).with_obs_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_obs_name("LX"), + stimflow.PauliMap.from_zs([0, 2]).with_obs_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_obs_name("LX"), + stimflow.PauliMap.from_zs([0, 1, 2, 3, 4]).with_obs_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_obs_name("LX1"), + stimflow.PauliMap.from_zs([0, 1j]).with_obs_name("LZ1"), + ), + ( + stimflow.PauliMap.from_xs([0, 1j]).with_obs_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_obs_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_obs_name("LX1"), + stimflow.PauliMap.from_zs([0, 2j]).with_obs_name("LZ1"), + ), + ( + stimflow.PauliMap.from_xs([0, 2j]).with_obs_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_obs_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_obs_name("LX1"), + stimflow.PauliMap.from_zs([0, 1j]).with_obs_name("LZ1"), + ), + ( + stimflow.PauliMap.from_xs([0, 1j]).with_obs_name("LX2"), + stimflow.PauliMap.from_zs([0, 1]).with_obs_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_obs_name("LX1"), + stimflow.PauliMap.from_zs([a, c]).with_obs_name("LX2"), + ), + ( + stimflow.PauliMap.from_zs([a, b]).with_obs_name("LZ1"), + stimflow.PauliMap.from_xs([a, c]).with_obs_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_obs_name("LX1"), + stimflow.PauliMap.from_zs([a, c]).with_obs_name("LZ1"), + ), + ( + stimflow.PauliMap.from_zs([a, b]).with_obs_name("LX2"), + stimflow.PauliMap.from_xs([a, c]).with_obs_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_obs_name("inferred_X0"), + stimflow.PauliMap({"Z": [0, 2]}).with_obs_name("inferred_Z0"), + ), + ( + 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/_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..94fb45bf --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/__init__.py @@ -0,0 +1,21 @@ +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 ( + 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..fa869c5b --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_complex_util.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from collections.abc import Callable, Iterable +from typing import Any, cast, TypeVar + + +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)] + """ + return sorted(values, key=lambda e: (e.real, e.imag)) + + +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 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 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: + 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. + + 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] + """ + kept = set() + for v in vals: + if v in kept: + kept.remove(v) + else: + 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/_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..4831bec2 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_flow.py @@ -0,0 +1,288 @@ +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, + measurement_indices: Iterable[int] = (), + 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). + 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_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. + sign: Defaults to None (unsigned). The expected sign of the flow. + """ + if start is not None and not isinstance(start, (PauliMap, Tile)): + 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 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.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 + if center is None and isinstance(end, Tile): + center = end.measure_qubit + + if isinstance(start, PauliMap): + obs_name = start.obs_name + elif isinstance(end, PauliMap): + obs_name = end.obs_name + else: + obs_name = None + if isinstance(start, Tile): + start = start.to_pauli_map().with_obs_name(obs_name) + elif start is None: + start = PauliMap(obs_name=obs_name) + if isinstance(end, Tile): + end = end.to_pauli_map().with_obs_name(obs_name) + elif end is None: + end = PauliMap(obs_name=obs_name) + + 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 + + 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_name is None: + included_observables = None + elif o2i is None: + raise ValueError(f"{self.obs_name=} is not None but {o2i=}") + else: + v = o2i[self.obs_name] + 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_name(self) -> Any: + return self.start.obs_name + + def with_edits( + self, + *, + start: PauliMap = _UNSPECIFIED, + end: PauliMap = _UNSPECIFIED, + measurement_indices: Iterable[int] = _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, + measurement_indices=( + self.measurement_indices + if measurement_indices is _UNSPECIFIED + else cast(Any, measurement_indices) + ), + 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_name == other.obs_name + 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_name, + 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_name is None else f" (obs={self.obs_name})" + 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"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 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_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: + 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, + 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), + ), + 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_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)") + + 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, + measurement_indices=xor_sorted(self.measurement_indices + other.measurement_indices), + 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/_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..b23bb358 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_noise.py @@ -0,0 +1,676 @@ +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, + allow_multiple_uses_of_a_qubit_in_one_tick: 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. + + 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( + 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}), + }, + allow_multiple_uses_of_a_qubit_in_one_tick=allow_multiple_uses_of_a_qubit_in_one_tick, + ) + + 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..3bb23e86 --- /dev/null +++ b/glue/stimflow/src/stimflow/_core/_pauli_map.py @@ -0,0 +1,306 @@ +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: + """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, + mapping: ( + dict[complex, Literal["X", "Y", "Z"] | str] + | dict[Literal["X", "Y", "Z"] | str, complex | Iterable[complex]] + | PauliMap + | Tile + | stim.PauliString + | None + ) = 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. + 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. + + 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"}, obs_name="test")) + (obs_name='test') X0*Y1*Z2 + """ + + self._dict: dict[complex, Literal["X", "Y", "Z"]] + self.obs_name: Any = obs_name + self._hash: int + + from stimflow._core._tile import Tile + + if isinstance(mapping, Tile): + self._dict = dict(mapping.to_pauli_map().items()) + elif isinstance(mapping, PauliMap): + self._dict = dict(mapping.items()) + elif isinstance(mapping, stim.PauliString): + self._dict = {q: cast(Any, "_XYZ"[mapping[q]]) for q in mapping.pauli_indices()} + elif mapping is not None: + 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)) + 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._dict = {complex(q): self._dict[q] for q in sorted_complex(self.keys())} + else: + self._dict = {} + 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}, 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}, 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}, obs_name=name) + + def __contains__(self, item: complex) -> bool: + """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._dict.items() + + def values(self) -> Iterable[Literal["X", "Y", "Z"]]: + """Returns the bases used by the PauliMap.""" + return self._dict.values() + + def keys(self) -> Set[complex]: + """Returns the qubits of the PauliMap.""" + return self._dict.keys() + + def get(self, key: complex, default: Any = None) -> Any: + return self._dict.get(key, default) + + def __getitem__(self, item: complex) -> Literal["I", "X", "Y", "Z"]: + return cast(Any, self._dict.get(item, "I")) + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self) -> Iterator[complex]: + return self._dict.__iter__() + + 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, obs_name=name) + + def _mul_term(self, q: complex, b: Literal["X", "Y", "Z"]): + new_b = _multiplication_table[self._dict.pop(q, None)][b] + if new_b is not None: + 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()}, obs_name=self.obs_name) + + def __bool__(self) -> bool: + return bool(self._dict) + + 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._dict.get(q, "I") + b = other._dict.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.obs_name is None: + s2 = "" + else: + s2 = f", obs_name={self.obs_name!r}" + 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._dict[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._dict[q]}{simplify(q)}" for q in sorted_complex(self.keys())) + if not result: + result = 'I' + 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()}, 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()}, obs_name=self.obs_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._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._dict.items()}, obs_name=self.obs_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._dict == other._dict + + def _sort_key(self) -> Any: + return tuple((q.real, q.imag, p) for q, p in self._dict.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..5bc482f7 --- /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._layer_interact import LayerInteract +from stimflow._layers._layer_circuit import LayerCircuit +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/_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/_layer.py b/glue/stimflow/src/stimflow/_layers/_layer.py new file mode 100644 index 00000000..04d338c8 --- /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 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 LayerRotation 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..287ac185 --- /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._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._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") + + +@dataclasses.dataclass +class LayerCircuit: + """A stabilizer circuit represented as a series of typed layers. + + 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) + + 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], 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(LayerReset) + for t in targets: + layer.targets[t.value] = basis + + def _feed_tag(self, instruction: stim.CircuitInstruction): + layer = self._feed(LayerTag) + layer.circuit.append(instruction) + + def _feed_m(self, basis: Literal["X", "Y", "Z"], targets: list[stim.GateTarget]): + 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(LayerMpp) + 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(LayerQubitCoordAnnotation) + 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(LayerShiftCoordAnnotation).offset_by(gate_args) + + def _feed_named_rotation_instruction(self, instruction: stim.CircuitInstruction): + 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(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: 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) + layer.i_layer.bases1.append("Z") + layer.i_layer.bases2.append("X") + + def _feed_swapcx(self, targets: list[stim.GateTarget]): + 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) + layer.i_layer.bases1.append("X") + layer.i_layer.bases2.append("Z") + + def _feed_iswap(self, targets: list[stim.GateTarget]): + 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(LayerSqrtPP) + 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: LayerFeedback = self._feed(LayerFeedback) + 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: LayerInteract = self._feed(LayerInteract) + 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( + LayerLoop( + 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(LayerEmpty()) + + 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(LayerNoise).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 = LayerQubitCoordAnnotation() + rev_layers: list[Layer] = [] + while k > 0: + k -= 1 + layer = self.layers[k] + 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, LayerShiftCoordAnnotation): + merged_layer.offset_by(layer.shift) + rev_layers.append(layer) + elif isinstance(layer, LayerLoop): + 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, LayerReset): + return layer.touched() + if isinstance(layer, LayerLoop): + 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, 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, LayerRotation): + 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], LayerRotation) + 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, LayerRotation): + 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: LayerRotation = cast(LayerRotation, 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, 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()]) + + 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, 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, LayerShiftCoordAnnotation), + ): + rot_layer_index -= 1 + continue + 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(LayerRotation, loop_layers.pop(rot_layer_index)) + loop_layers.insert(0, popped) + + new_layers.append(popped.inverse()) + new_layers.append( + LayerLoop(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, LayerRotation) 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, LayerRotation): + 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: 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, 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: 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, LayerRotation): + 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, LayerLoop): + 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( + LayerLoop(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, LayerEmpty): + pass + elif isinstance(layer, LayerLoop): + new_layers.append(LayerLoop(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, 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: + 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] = LayerLoop(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, LayerMeasure): + m1: LayerMeasure = cur_layer + k2 = k + 1 + while k2 < len(self.layers) and isinstance( + self.layers[k2], (DetObsAnnotationLayer, LayerShiftCoordAnnotation) + ): + k2 += 1 + 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( + LayerMeasure(targets=m1.targets + m2.targets, bases=m1.bases + m2.bases) + ) + for k3 in range(k + 1, k2): + l3: DetObsAnnotationLayer | LayerShiftCoordAnnotation + 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 = ( + LayerReset, + LayerInteract, + LayerFeedback, + LayerRotation, + LayerSwap, + LayerISwap, + LayerInteractSwap, + LayerEmpty, + ) + 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..ff296080 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_circuit_test.py @@ -0,0 +1,794 @@ +import stim + +from stimflow._layers._layer_interact import LayerInteract +from stimflow._layers._layer_circuit import LayerCircuit +from stimflow._layers._layer_reset import LayerReset + + +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(LayerReset) + .with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(LayerInteract) + == 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/_layer_det_obs_annotation.py b/glue/stimflow/src/stimflow/_layers/_layer_det_obs_annotation.py new file mode 100644 index 00000000..623f29f4 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_det_obs_annotation.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/_layer_empty.py b/glue/stimflow/src/stimflow/_layers/_layer_empty.py new file mode 100644 index 00000000..a62e3f49 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_empty.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class LayerEmpty(Layer): + def copy(self) -> LayerEmpty: + return LayerEmpty() + + 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/_layer_feedback.py b/glue/stimflow/src/stimflow/_layers/_layer_feedback.py new file mode 100644 index 00000000..10b94e2b --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_feedback.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 LayerRotation + + +@dataclasses.dataclass +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) -> LayerFeedback: + result = self.copy() + result.controls = [stim.target_rec(t.value + shift) for t in result.controls] + return result + + def copy(self) -> LayerFeedback: + return LayerFeedback( + 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: LayerRotation) -> LayerFeedback: + return LayerFeedback( + 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/_layer_feedback_test.py b/glue/stimflow/src/stimflow/_layers/_layer_feedback_test.py new file mode 100644 index 00000000..97e4f540 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_feedback_test.py @@ -0,0 +1,7 @@ +from stimflow._layers._layer_feedback 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/_layer_interact.py b/glue/stimflow/src/stimflow/_layers/_layer_interact.py new file mode 100644 index 00000000..0e2c154b --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_interact.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import collections +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class LayerInteract(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) -> LayerInteract: + return LayerInteract( + 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._layer_rotation import LayerRotation + + result = LayerRotation() + 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, + LayerInteract( + 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._layer_interact_swap import LayerInteractSwap + from stimflow._layers._layer_swap import LayerSwap + + 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 [LayerInteractSwap(i_layer=self.copy())] + elif isinstance(next_layer, LayerInteract) and self.touched().isdisjoint( + next_layer.touched() + ): + return [ + LayerInteract( + 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/_layer_interact_swap.py b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap.py new file mode 100644 index 00000000..d913846b --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer_interact import LayerInteract +from stimflow._layers._layer import Layer +from stimflow._layers._layer_swap import LayerSwap + + +@dataclasses.dataclass +class LayerInteractSwap(Layer): + i_layer: LayerInteract = dataclasses.field(default_factory=LayerInteract) + + def copy(self) -> LayerInteractSwap: + return LayerInteractSwap(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 = LayerInteract() + 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") + 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(), + 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), + ) + ), + LayerInteract( + 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/_layer_interact_swap_test.py b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap_test.py new file mode 100644 index 00000000..8f281a6c --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_interact_swap_test.py @@ -0,0 +1,59 @@ +import stim + +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 = 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 == [ + 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"], + ) + ), + LayerRotation({0: "H_YZ", 1: "H", 2: "H"}), + ] + + +def test_append_into_circuit(): + layer = LayerInteractSwap( + i_layer=LayerInteract( + 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 = LayerInteractSwap( + i_layer=LayerInteract( + 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/_layer_iswap.py b/glue/stimflow/src/stimflow/_layers/_layer_iswap.py new file mode 100644 index 00000000..5a5e0894 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_iswap.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +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) -> LayerISwap: + return LayerISwap(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_loop.py b/glue/stimflow/src/stimflow/_layers/_layer_loop.py new file mode 100644 index 00000000..9530c88a --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_loop.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 LayerLoop(Layer): + body: LayerCircuit + repetitions: int + + 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 [LayerLoop(body=self.body.to_z_basis(), repetitions=self.repetitions)] + + def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: + optimized = LayerLoop( + 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/_layer_measure.py b/glue/stimflow/src/stimflow/_layers/_layer_measure.py new file mode 100644 index 00000000..1e262183 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_measure.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer +from stimflow._layers._layer_rotation import LayerRotation + + +@dataclasses.dataclass +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) -> 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 = LayerRotation( + { + q: "I" if b == "Z" else "H" if b == "X" else "H_YZ" + for q, b in zip(self.targets, self.bases) + } + ) + return [ + rot, + LayerMeasure(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, LayerMeasure) and set(self.targets).isdisjoint( + next_layer.targets + ): + return [ + LayerMeasure( + targets=self.targets + next_layer.targets, bases=self.bases + next_layer.bases + ) + ] + if isinstance(next_layer, LayerRotation) 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/_layer_mpp.py b/glue/stimflow/src/stimflow/_layers/_layer_mpp.py new file mode 100644 index 00000000..d23b9370 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_mpp.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class LayerMpp(Layer): + targets: list[list[stim.GateTarget]] = dataclasses.field(default_factory=list) + + 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} + + 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/_layer_noise.py b/glue/stimflow/src/stimflow/_layers/_layer_noise.py new file mode 100644 index 00000000..756fb257 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_noise.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class LayerNoise(Layer): + """A layer of noise operations.""" + + circuit: stim.Circuit = dataclasses.field(default_factory=stim.Circuit) + + def copy(self) -> LayerNoise: + return LayerNoise(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/_layer_qubit_coord_annotation.py b/glue/stimflow/src/stimflow/_layers/_layer_qubit_coord_annotation.py new file mode 100644 index 00000000..ce270c92 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_qubit_coord_annotation.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 LayerQubitCoordAnnotation(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 LayerQubitCoordAnnotation(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/_layer_reset.py b/glue/stimflow/src/stimflow/_layers/_layer_reset.py new file mode 100644 index 00000000..f4941f8c --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_reset.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._layer_rotation import LayerRotation + + +@dataclasses.dataclass +class LayerReset(Layer): + """A layer of reset gates.""" + + targets: dict[int, Literal["X", "Y", "Z"]] = dataclasses.field(default_factory=dict) + + 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 [ + 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() + } + ), + ] + + 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, LayerReset): + return [ + LayerReset( + 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/_layer_rotation.py b/glue/stimflow/src/stimflow/_layers/_layer_rotation.py new file mode 100644 index 00000000..472c5568 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_rotation.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 LayerRotation(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) -> LayerRotation: + return LayerRotation(dict(self.named_rotations)) + + def inverse(self) -> LayerRotation: + t = single_qubit_clifford_inverse_table() + 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]] = {} + 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._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, LayerShiftCoordAnnotation)): + return [next_layer, self] + if isinstance(next_layer, LayerFeedback): + return [next_layer.before(self), self] + if isinstance(next_layer, LayerReset): + 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, LayerRotation): + 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/_layer_rotation_test.py b/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py new file mode 100644 index 00000000..27d88f2e --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_rotation_test.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import stim + +from stimflow._layers._layer_rotation import LayerRotation + + +def test_fuses_rotations(): + 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 = LayerRotation() + + 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/_layer_shift_coord_annotation.py b/glue/stimflow/src/stimflow/_layers/_layer_shift_coord_annotation.py new file mode 100644 index 00000000..09728802 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_shift_coord_annotation.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 LayerShiftCoordAnnotation(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) -> LayerShiftCoordAnnotation: + return LayerShiftCoordAnnotation(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, 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) -> LayerShiftCoordAnnotation: + return self.copy() diff --git a/glue/stimflow/src/stimflow/_layers/_layer_sqrt_pp.py b/glue/stimflow/src/stimflow/_layers/_layer_sqrt_pp.py new file mode 100644 index 00000000..2fdac1a2 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_sqrt_pp.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import collections +import dataclasses + +import stim + +from stimflow._layers._layer_interact import LayerInteract +from stimflow._layers._layer import Layer +from stimflow._layers._layer_rotation import LayerRotation + + +@dataclasses.dataclass +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) + + def touched(self) -> set[int]: + return set(self.targets1 + self.targets2) + + 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 = LayerInteract() + rot = LayerRotation() + 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/_layer_swap.py b/glue/stimflow/src/stimflow/_layers/_layer_swap.py new file mode 100644 index 00000000..fad58910 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_swap.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import dataclasses + +import stim + +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._layer_shift_coord_annotation import LayerShiftCoordAnnotation + + +@dataclasses.dataclass +class LayerSwap(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) -> LayerSwap: + return LayerSwap(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, 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 [LayerInteractSwap(i_layer=i)] + if isinstance(next_layer, (LayerShiftCoordAnnotation, DetObsAnnotationLayer)): + return [next_layer, self] + if isinstance(next_layer, LayerSwap): + total_swaps = self.to_swap_dict() + leftover_swaps = LayerSwap() + 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 = LayerSwap() + 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/_layer_tag.py b/glue/stimflow/src/stimflow/_layers/_layer_tag.py new file mode 100644 index 00000000..e858ca74 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_tag.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import dataclasses + +import stim + +from stimflow._layers._layer import Layer + + +@dataclasses.dataclass +class LayerTag(Layer): + circuit: stim.Circuit = dataclasses.field(default_factory=stim.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] + 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/_layer_tag_test.py b/glue/stimflow/src/stimflow/_layers/_layer_tag_test.py new file mode 100644 index 00000000..decfa8b6 --- /dev/null +++ b/glue/stimflow/src/stimflow/_layers/_layer_tag_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..2111a26c --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_3d_model.py @@ -0,0 +1,563 @@ +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 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: 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. + + 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; 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 height of the rectangle is determined + automatically from the text. + Should be perpendicular to `forward`. + 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). + """ + 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 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: + 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). + + 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 + ) + 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], + ) -> TriangleDataFor3DModel: + """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. + """ + np_origin = np.array(origin, dtype=np.float32) + d1 = np.array(d1, dtype=np.float32) + d2 = np.array(d2, dtype=np.float32) + p1 = np_origin + d1 + p2 = np_origin + d2 + return TriangleDataFor3DModel( + rgba=rgba, + triangle_list=np.array([ + [np_origin, p1, p2], + [p2, p1, p1 + d2], + ], dtype=np.float32) + ) + + @staticmethod + def fused(data: Iterable[TriangleDataFor3DModel]) -> list[TriangleDataFor3DModel]: + """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( + TriangleDataFor3DModel( + rgba=rgba, + triangle_list=np.concatenate([e.triangle_list for e in group], axis=0), + ) + ) + return result + + +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: + 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). + """ + np_edge_list = np.asarray(edge_list, dtype=np.float32) + assert ( + len(np_edge_list.shape) == 3 + and np_edge_list.shape[1] == 2 + and np_edge_list.shape[2] == 3 + ) + assert len(rgba) == 4 + self.rgba: tuple[float, float, float, float] = cast(Any, tuple(rgba)) + self.edge_list: np.ndarray = np_edge_list + + @staticmethod + def fused(data: Iterable[LineDataFor3DModel]) -> list[LineDataFor3DModel]: + """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( + LineDataFor3DModel( + rgba=rgba, edge_list=np.concatenate([e.edge_list for e in group], axis=0) + ) + ) + return result + + +def make_3d_model( + elements: Iterable[TriangleDataFor3DModel | LineDataFor3DModel | TextDataFor3DModel], +) -> Viewable3dModelGLTF: + """Creates a 3d model containing the given elements. + + Args: + elements: A list of objects to include in the model. The list can include triangles + (TriangleDataFor3DModel), lines (LineDataFor3DModel), and text (TextDataFor3DModel). + + 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. + + 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] + "...") + + + + + + + = 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 = LineDataFor3DModel.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..50ae3fd8 --- /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 TextDataFor3DModel + + +def test_make_3d_model(): + model = stimflow.make_3d_model( + [ + stimflow.TriangleDataFor3DModel( + rgba=(1, 0, 0, 1), + triangle_list=np.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]], dtype=np.float32), + ), + stimflow.TriangleDataFor3DModel( + 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.TriangleDataFor3DModel( + rgba=(1, 0, 0, 1), + triangle_list=np.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]], dtype=np.float32), + ), + stimflow.TriangleDataFor3DModel( + 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.TriangleDataFor3DModel( + rgba=(1, 0, 0, 1), + triangle_list=np.array([[[0, 0, 0], [1, 0, 0], [0, 1, 0]]], dtype=np.float32), + ), + stimflow.LineDataFor3DModel( + rgba=(0, 0, 1, 1), edge_list=np.array([[[0, 0, 0], [1, 1, 1]]], dtype=np.float32) + ), + TextDataFor3DModel(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 = """ +iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAYAAAB/HSuDAAAgAElEQVR42uzdd3gU9f638fcmgQAJ +HQxSBASkSFVAkSKoiIgiioAQBaKo9N6CnB+KeEAR6cWOItVGswDSRWlSA0gTkBIQpJeElHn+gPhw +kOzMbrZO7vu69rrOMd/M7mt2E5JPZmcchmSIiIiIiIiIiGxdCLuAiIiIiIiIiAEAERERERERETEA +ICIiIiIiIiIGAERERERERETEAGC7pJ8y8c7Hjx8/fvz48ePHjx8/fvz4bT8A+FlSPUlNJM3OhE8+ +fvz48ePHjx8/fvz48ePH79MMyfD1bYFkZL92+UFDkhEiGeP98Dj8dcOPHz9+/Pjx48ePHz9+/Ph9 +ffP5AGCqZITdgL/x9p9M8OTjx48fP378+PHjx48fP378/7H7AGCkZDjSwafdOkpGik2ffPz48ePH +jx8/fvz48ePHj99ffp8NAPqbwG+8PSsZiTZ78vHjx48fP378+PHjx48fP35/+r0+AEiWjBgX8Gm3 +hyXjvA2eePz48ePHjx8/fvz48ePHjz8Q/F4dAFyRjKZu4NNu90rGX0H85OPHjx8/fvz48ePHjx8/ +fvyB4vfaAOCsZNTNAD7tdpdkHAzCJx8/fvz48ePHjx8/fvz48eMPJL9XBgDHJKOyB/Bpt8KSsT2I +nnz8+PHjx48fP378+PHjx48/0PweHwDslYySHsSn3fJKxpogePLx48ePHz9+/Pjx48ePHz/+QPR7 +dACwSTKivIBPu+WQjO8C+MnHjx8/fvz48ePHjx8/fvz4A9XvsQHAcsnI5UV82i1MMj4PwCcfP378 ++PHjx48fP378+PHjD2S/RwYA30pGuA/waTeHZLwXQE8+fvz48ePHjx8/fvz48ePHH+j+DA8APpKM +UB/ib7wNDIAnHz9+/Pjx48ePHz9+/Pjx4w8Gv8O4tjG3Gi5pkJufGyopSlKkpOySskpKuH47Iem8 +xe28JOn969vzdfjx48ePHz9+/Pjx48ePH3+w+N0aABiSeksaY3H9XZLqSaoiqbKkMtfxIU4+55Kk +PZK2SlorabGkA+msbSZppqRsPnri8ePHjx8/fvz48ePHjx8//qDzu3rIQJJkPG9yaEIWyWh0/fCI +Yx483GKLZPSQjNy3uM8HJeOcDw75wI8fP378+PHjx48fP378+IPR79IA4JJkNHYCLy0Z70jGSS8/ +CWckY4hkZL/p/qtKRrwX7xc/fvz48ePHjx8/fvz48eMPVr/lAcDfklErHXj162dCTPXxCRj+kIyH +bnosd0rGPi/cF378+PHjx48fP378+PHjxx/MfksDgCOScfct4OUlY4Gfz8KYfP1siDc+rkLXD5fw +1H3gx48fP378+PHjx48fP378we43HQD8Lhl33AL/3vX3QxgBchtz0+PLLRkrPbBd/Pjx48ePHz9+ +/Pjx48eP3w5+pwOADZJRIJ3DHk4GED7tNuymx5hNMuZmYHv48ePHjx8/fvz48ePHjx+/XfzpDgCW +SEak0j/hQSDuAEMyWtz0OEMl42M3toMfP378+PHjx48fP378+PHbye8wri38n+ZIekHSVSeXDzwp +qYCLlxy8KGm9pJ2S9kk6c/16hzkl5ZNUVNL9ku6RFO7mNRpPSapw/fHd2HBJAy1uAz9+/Pjx48eP +Hz9+/Pjx47ed/+aJwPuSESLn1zl0ZQJyQDJGSMYDkhFmYbuSjByS0SEDJ3KYmM52+1r4XPz48ePH +jx8/fvz48ePHj9+O/n8NAGpbfJDOdkCyZHwtGfUtbiu9m0Myuura9Rdd2QGJklH4FtvLb+Fz8ePH +jx8/fvz48ePHjx8/fjv6PToASLr+foOSGYTffLtbMk64uBMG+OEFgB8/fvz48ePHjx8/fvz48Qeq +32MDgK8lo7SH4TfeKkvGWRd2wGYfvwDw48ePHz9+/Pjx48ePHz/+QPZneACwUzIe9iL8xlsnF3ZA +qmTk9cELAD9+/Pjx48ePHz9+/Pjx4w8Gv9sDgHhdu+5gVh/hpWsnZ9jhwk5o6MUXAH78+PHjx48f +P378+PHjxx9M/hA3rzag+pIGm1wqwdOlSprpwvrSXnws+PHjx48fP378+PHjx48ffzD53R4A7JZ/ ++s6FtUW8+Djw48ePHz9+/Pjx48ePHz/+YPKHKMg65MLaCNkv/Pjx48ePHz9+/Pjx48eP3x1/0A0A +TktKsrg21IYvAPz48ePHjx8/fvz48ePHj98df9ANAEIkhVlce86GLwD8+PHjx48fP378+PHjx4/f +HX/QDQBKSHJYXBtvwxcAfvz48ePHjx8/fvz48ePH744/6AYAdVxYG2fDFwB+/Pjx48ePHz9+/Pjx +48fvjj/oBgBtLK5LkLTRhi8A/Pjx48ePHz9+/Pjx48eP3x1/UA0AqklqZHHtUkmXbfbk48ePHz9+ +/Pjx48ePHz9+/O76g2YAECppsgvrP7DZk48fP378+PHjx48fP378+PFnxB80A4Chku6zuPZ3SQtt +9gLAjx8/fvz48ePHjx8/fvz4M+Q3JOPGW23JUIDd2khG6k2P09mtYTrbyW/hc/Hjx48fP378+PHj +x48fP347+gP+CIDHJH0q65c+mCZpiY0mP/jx48ePHz9+/Pjx48ePH79H/IE8AWkkGQkuTD52S0ak +k+0F2wQIP378+PHjx48fP378+PHj95Q/YI8AaCxpnqRwi+vPS2ou6aJNJj/48ePHjx8/fvz48ePH +jx+/J/0BOQB4XNK3LuCTJT0rKc4mTz5+/Pjx48ePHz9+/Pjx48fvaX/ADQCecBFvSOog+7zvAz9+ +/Pjx48ePHz9+/Pjx4/eKP5DeA/GUZCS68J4HQzI6urD9QH8PCH78+PHjx48fP378+PHjx+8tf8AM +AJpJxlUX8d1dvI9AfgHgx48fP378+PHjx48fP3783vQHxADgMRcnH6mS0dWN+wnUFwB+/Pjx48eP +Hz9+/Pjx48fvbb/fBwD1JOOyi/iX3byvQHwB4MePHz9+/Pjx48ePHz9+/L7w+3UAUFoy/nYBnyIZ +bTNwf4H2AsCPHz9+/Pjx48ePHz9+/Ph95Q/z19kOc0paICmfxfXJktpKmmmTsz3ix48fP378+PHj +x48fP378vvT7bQAwRlI5F/BtJH0p+4QfP378+PHjx48fP378+PH71O+PQyAau3DYQ7JktPTQ/QbK +ISD48ePHjx8/fvz48ePHjx+/r/0+HwCESEacCzvgFQ/edyC8APDjx48fP378+PHjx48fP35/+EN8 +fehDc0l3W1z7rqQPZK/w48ePHz9+/Pjx48ePHz9+f/h9PgB40eK6tZJiZb/w48ePHz9+/Pjx48eP +Hz9+f/h9OgDII6mhxbVddO3kB3YKP378+PHjx48fP378+PHj95ffpwOAupJCLaz7TdIm2S/8+PHj +x48fP378+PHjx4/fX36fDgDut7huvuwZfvz48ePHjx8/fvz48ePH7y+/TwcApS2uW2XTFwB+/Pjx +48ePHz9+/Pjx48fvL79PBwAlLK6Lt+kLAD9+/Pjx48ePHz9+/Pjx4/eX36cDgNwW15206QsAP378 ++PHjx48fP378+PHj95ffpwOAHBbXnbfpCwA/fvz48ePHjx8/fvz48eP3l9+nA4AsFtcZNn0B4MeP +Hz9+/Pjx48ePHz9+/P7yh4iIiIiIiIiIbB8DACIiIiIiIiIGAERERERERETEAICIiIiIiIiIGAAQ +EREREREREQMAIiIiIiIiImIAQERERERERESeKsyXd1ZKksPCuhSb7mz8+PHjx48fP378+PHjx4/f +X36fDgAuK3OHHz9+/Pjx48ePHz9+/Pjx+yveAkBERERERESUCWIAQERERERERJQZBwBVbIyt4qE1 ++PHjx48fP378+PHjx48ff9D5Dcm48ZYiGZ0lQza7NZGMyzdZb3XDjx8/fvz48ePHjx8/fvz47ehX +ejtiiI3wbSUjycKTjx8/fvz48ePHjx8/fvz48dvV7zCuLbplkyR1k5TqgcMPCki6z+La3yQd99Bh +D30kjZS1yy/gx48fP378+PHjx48fP378tvWbTUJmSUZWD0whHnZh+tLcQ5OPES5OffDjx48fP378 ++PHjx48fP367+k2vAtBK0kJJEQqeQiV9ImmAB7aFHz9+/Pjx48ePHz9+/Pjx28Fv6TKADSUtk5Q/ +CPDZJX0jKcaD28SPHz9+/Pjx48ePHz9+/PiD3R9idcM1Jf0sqVgA4/NIWiypqRe2jR8/fvz48ePH +jx8/fvz48QezP8SVOygn6RdJ5QMQf7ukVZLqePE+8OPHjx8/fvz48ePHjx8//mD1h7h6R0UlrZb1 +Mxr6otKS1kiq5IP7wo8fP378+PHjx48fP378+IPRH+LOHeaXtFRSowDA33MdX9KH94kfP378+PHj +x48fP378+PEHmz/E3TuOkLRAUms/4htIWiHpNj/cN378+PHjx48fP378+PHjxx9M/pCMPIAskqZL +6uYHfHNJP0jK6ccnAD9+/Pjx48ePHz9+/Pjx4w8Wf0hGH4hD0jhJQ32If0XSHEnh8n/48ePHjx8/ +fvz48ePHjx9/MPhDPPWg/iNpsic3mE6DJb3vg/vBjx8/fvz48ePHjx8/fvz47eT36OPtKGmWpKxe +gKdNWt5U4IYfP378+PHjx48fP378+PEHqj/M0w+0haS8kp6WdPGG/54s6azFbSTd9P+zSPpM/j3h +An78+PHjx48fP378+PHjxx/UfkMyvHHbIBkFJUMZvEVIxo9eeozevOHHjx8/fvz48ePHjx8/fvyB +5Jc3d8LvklE8A/j8krEuCJ98/Pjx48ePHz9+/Pjx48ePP9D88vZOOCIZd7uBLyYZu4L4ycePHz9+ +/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..0e66af37 --- /dev/null +++ b/glue/stimflow/src/stimflow/_viz/_3d_model_viewer.py @@ -0,0 +1,211 @@ +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..da3eb959 --- /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 ( + LineDataFor3DModel, + TriangleDataFor3DModel, + TextDataFor3DModel, + 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()